Golang中的sync.WaitGroup陷阱与规避 Go语言Add与Wait调用时机

5次阅读

waitgroup.add必须在goroutine启动前调用,否则wait可能提前返回;不能复制waitgroup,须传指针;done调用次数必须严格等于add总和;不适用于等待已启动的goroutine。

Golang中的sync.WaitGroup陷阱与规避 Go语言Add与Wait调用时机

WaitGroup.Add 必须在 goroutine 启动前调用

很多人把 Add(1) 放在 goroutine 内部,结果 Wait() 立刻返回,后续逻辑出错。本质是:WaitGroup 计数器必须在 goroutine 开始执行前就“预约”好,否则调度器可能在 Add 之前就让主 goroutine 跑完 Wait

  • 错误写法:go func() { wg.Add(1); defer wg.Done(); ... }() —— AddDone 在同一 goroutine 里,但 Wait 已无计数可等
  • 正确顺序:先 wg.Add(1),再 go func() { defer wg.Done(); ... }()
  • 批量启动时,Add(n) 一定要在所有 go 语句之前,不能放在循环体内部(除非你明确要动态增减)

WaitGroup 不能被复制,必须传指针

Go 中结构体默认值传递sync.WaitGroup 包含 mutex 和原子变量,复制后两个实例完全独立,原 WaitGroupAdd/Done 对副本无效,Wait 永远阻塞或提前返回。

  • 常见错误:函数参数写 func process(wg sync.WaitGroup),然后传入 wg 变量 —— 实际上传的是副本
  • 必须写成 func process(wg *sync.WaitGroup),调用时传 &wg
  • 方法接收者也同理:如果定义了 func (w *Worker) Start(wg sync.WaitGroup),就是错的;得是 func (w *Worker) Start(wg *sync.WaitGroup)

Done 调用次数必须严格等于 Add 的总和

Done()Add(-1) 的别名,多次调用或漏调都会导致 panic 或死锁。运行时检测到负计数会直接 panic:panic: sync: negative WaitGroup counter

  • 常见错误:recover 捕获 panic 后没处理 Done,或者分支逻辑中某条路径漏掉 defer wg.Done()
  • 推荐写法:始终用 defer wg.Done(),且确保它在 goroutine 最外层作用域(不要包在 if 或子函数里)
  • 调试技巧:在 Done 前加日志,如 log.printf("done, remain=%d", wg.counter)(需反射读取,生产慎用),或用 -race 检测竞态

WaitGroup 不适用于等待“已启动”的 goroutine

WaitGroup 只能协调“你主动启动”的 goroutine,无法感知外部 goroutine 的生命周期。比如启动一个长期运行的服务协程,之后想用 Wait 等它退出 —— 这不行,因为 Add 已调过,Done 却不知何时触发。

立即学习go语言免费学习笔记(深入)”;

  • 典型误用场景:在 http.ListenAndServe 后调 wg.Wait(),以为能等服务器关闭 —— 实际上服务器协程不调 DoneWait 永远卡住
  • 替代方案:用 context.Context 控制生命周期,配合 channel 通知退出;或用 sync.Once + sync.Cond 手动同步状态
  • WaitGroup 的定位很窄:只管“我启了多少、它们干完没”,不管“它们在干什么、能不能中断、有没有超时”

最易被忽略的一点:WaitGroup 的 zero value 是可用的,但一旦开始使用(Add/Wait/Done),就不能再赋值或重新声明同名变量 —— Go 不报错,但语义已乱。别把它当普通 Struct 随便重置。

text=ZqhQzanResources