sync.WaitGroup 怎么用?有哪些常见坑?

结论先行

WaitGroup 是“等待一组 goroutine 收工”的计数器:Add(n) 增加计数,每个 goroutine 完成后调用 Done()(等价 Add(-1)),计数归零时 Wait() 返回。用错它的问题高度集中在三个词:Add 太晚、Done 漏调、Wait 与 Add 并发。

要点

  • 标准姿势:main 里先 Add 再启动 goroutine,goroutine 内用 defer wg.Done() 收尾,最后 wg.Wait() 阻塞到计数归零。
  • 计数规则:计数不允许为负,否则 panic(negative WaitGroup counter);Done 次数必须与 Add 精确匹配。
  • Add 时机:必须在 Wait 之前、且在启动 goroutine 之前完成;把 Add 写进 goroutine 内部会让 Wait 可能先跑完直接返回,形成经典竞态。
  • 复用约束:计数归零后可再次使用,但要保证上一次 Wait 已返回、且新一轮 Add 不与 Wait 并发,否则行为未定义。
  • 拷贝陷阱:WaitGroup 内含状态,按值传递或拷贝会让计数器“分身”,谁也等不到谁;必须传指针或放闭包。
  • panic 交互:goroutine 内 panic 若未被 recover,Done 不会执行,Wait 永久阻塞——每个 goroutine 入口做 defer/recover 兜底。
  • 动态任务:任务数量运行期才确定时,可采用“生产者 Add、消费者 Done”模式,但必须保证最后一次 Add 先于 Wait。

示例

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
	wg.Add(1) // 在 go 之前 Add
	go func(i int) {
		defer wg.Done() // 用 defer 保证一定执行
		work(i)
	}(i)
}
wg.Wait()
fmt.Println("all done")

常见追问/记忆点

  • 追问:WaitGroup 能被复制吗?答:不能,拷贝即“分身”,go vet 的 copylocks 会告警;共享用指针。
  • 追问:Add 放在 goroutine 里有什么风险?答:goroutine 调度晚于 Wait 执行时,Wait 看到计数为 0 提前返回。
  • 追问:可以复用同一个 WaitGroup 吗?答:计数归零后可再次使用,但上一轮 Wait 返回前不要开始新一轮 Add。
  • 记忆:Add 先行、Done 用 defer、Wait 殿后;不复制、不同时 Add 与 Wait。
笔记加载中…