1. Go 并发基础回顾:goroutine 的创建与生命周期、sync.WaitGroup 的使用、竞态条件与互斥锁 sync.Mutex
在深入 context 超时控制与错误传播之前,必须夯实 Go 并发编程的三大基石:goroutine(轻量级线程)、同步原语(WaitGroup / Mutex)以及竞态条件的认知。本章将系统梳理这些基础概念,为后续中间件链路中的并发协调打下坚实基础。
1.1 goroutine 的创建与生命周期
goroutine 是 Go 运行时管理的轻量级执行单元,由 go 关键字启动。其创建成本极低(初始栈仅 2KB),可动态扩缩,是构建高并发系统的核心。
关键特性:
- 非阻塞启动:
go f()立即返回,不等待函数执行完毕。 - 主 goroutine 退出即终止: 若
main函数返回,所有子 goroutine 会被强制结束。 - 生命周期不可控: 无法从外部杀死一个 goroutine,只能通过 channel 或 context 通知其自行退出。
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d 开始\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Worker %d 结束\n", id)
}
func main() {
go worker(1) // 启动 goroutine
go worker(2)
time.Sleep(2 * time.Second) // 等待 goroutine 完成(不推荐)
fmt.Println("主 goroutine 退出")
}
⚠️ 常见陷阱: 主 goroutine 不会等待子 goroutine 结束。上述代码使用
time.Sleep 是极不稳定的做法,必须使用 sync.WaitGroup 或 channel 进行同步。
1.2 sync.WaitGroup:等待 goroutine 集合完成
sync.WaitGroup 是 Go 标准库提供的计数信号量,用于等待一组 goroutine 全部执行完毕。其内部维护一个计数器,通过 Add、Done、Wait 三个方法协作。
| 方法 | 作用 | 典型调用时机 |
|---|---|---|
Add(delta int) |
增加计数器(通常 +1) | 启动 goroutine 前 |
Done() |
计数器减 1(等价于 Add(-1)) | goroutine 执行完毕时 |
Wait() |
阻塞直到计数器归零 | 主 goroutine 中等待所有子任务 |
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 确保计数器减 1
fmt.Printf("Worker %d 开始\n", id)
time.Sleep(time.Duration(id) * 500 * time.Millisecond)
fmt.Printf("Worker %d 结束\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 启动前加 1
go worker(i, &wg) // 传递指针
}
wg.Wait() // 等待所有 worker 完成
fmt.Println("所有 worker 已完成,主 goroutine 退出")
}
💡 最佳实践:
Add 必须在 go 启动之前调用,且 wg 必须通过指针传递,否则 goroutine 会操作副本导致死锁。
1.3 竞态条件与互斥锁 sync.Mutex
当多个 goroutine 并发读写同一共享变量,且至少一个操作为写操作时,就会产生竞态条件(Race Condition)。其结果不可预测,是并发编程中最隐蔽的 bug。
竞态条件三要素:
- 并发访问: 至少两个 goroutine 同时操作同一内存地址。
- 至少一个写操作: 纯读操作不会产生竞态。
- 无同步机制: 未使用锁、channel 或原子操作保护临界区。
sync.Mutex 提供互斥锁,确保同一时刻只有一个 goroutine 进入临界区。其接口简洁:Lock() 获取锁,Unlock() 释放锁。
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++ // 临界区:受锁保护
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := &Counter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("最终计数: %d (期望 1000)\n", counter.Value())
}
⚠️ 死锁风险: 忘记
Unlock() 或重复 Lock() 会导致死锁。始终使用 defer mu.Unlock() 确保释放。
1.4 并发基础概念关系图
下图展示了 goroutine、WaitGroup、Mutex 在并发程序中的协作关系:
1.5 竞态检测与避坑指南
Go 提供了内置的竞态检测器(Race Detector),在测试或运行时加上 -race 标志即可启用:
go run -race main.go
go test -race ./...
常见竞态场景与解决方案:
| 场景 | 问题描述 | 解决方案 |
|---|---|---|
| map 并发读写 | 多个 goroutine 同时读写 map 导致 panic | 使用 sync.RWMutex 或 sync.Map |
| 闭包捕获循环变量 | goroutine 启动时捕获了循环变量的引用 | 显式传递副本:go func(i int) { ... }(i) |
| 切片并发 append | 多个 goroutine 同时 append 同一切片 | 加锁或使用 channel 串行化 |
| 全局变量无保护 | 多个 goroutine 读写全局计数器 | 使用 sync.Mutex 或原子操作 sync/atomic |
1.6 本章小结
- goroutine 是 Go 并发的最小单元,由
go关键字启动,生命周期与主 goroutine 绑定。 - sync.WaitGroup 提供 goroutine 集合的同步等待机制,通过
Add/Done/Wait协作。 - 竞态条件 源于无保护的并发写操作,sync.Mutex 通过互斥锁保证临界区的原子性。
- 始终使用
go run -race检测竞态,这是生产级代码的必备实践。