sync.WaitGroup仅计数不管理goroutine生命周期;常见错误包括Add()位置不当、Done()遗漏、值传递导致状态分裂,正确做法是Add()前置、defer Done()入口统一、传指针。

sync.WaitGroup 本身不负责协程回收——它只计数,不管理 goroutine 生命周期。所谓“无法正常回收”,实际是程序卡在 wg.Wait() 上,或 goroutine 因漏调 wg.Done() 而永远阻塞,导致主 goroutine 不退出、进程不结束。
wg.Add() 放错位置导致 Wait() 立刻返回或 panic
常见错误是把 Add() 写进 goroutine 内部,尤其配合闭包循环变量时:
for i := 0; i
-
Add()和Wait()都不是线程安全的“协调操作”,它们依赖顺序约定:所有Add()必须发生在任何Wait()调用之前。 - 如果
Wait()先执行,而Add()还没跑,计数器为 0 →Wait()直接返回,后续 goroutine 仍在后台跑,但主函数已退出。 - 如果
Add(-1)(等价于Done())在计数器为 0 时被调,会 panic:panic: sync: negative WaitGroup counter。
正确做法:
-
wg.Add(1)必须写在go func() {...}()之前; - 循环启动 N 个 goroutine,就提前
wg.Add(N),或每次循环wg.Add(1); - 切勿在 goroutine 中动态
Add(),除非你额外加锁——但这违背WaitGroup的轻量设计初衷。
defer wg.Done() 被遗漏或未执行
这是最隐蔽的卡死原因:程序不报错、CPU 低、日志停在某处、wg.Wait() 永远不返回。
典型场景:
- 函数有多个
return分支,只在末尾写了wg.Done(),但中间 error 分支直接 return; - goroutine 内部 panic,且没 recover,
defer不触发; -
Done()写在条件分支里,某条路径绕过了它。
例如:
func worker(id int, wg *sync.WaitGroup) {
if id == 0 {
return // ❌ 忘记 wg.Done(),计数器永远少减 1
}
defer wg.Done() // 这行根本不会执行
// ...
}
解决方式只有一条:
AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。
- 所有 goroutine 入口第一行写
defer wg.Done(); - 不要用
defer wg.Add(-1)——Add()不是原子减法,语义错且线程不安全; - 如果内部还启子 goroutine,父级
Done()不代表子任务完成,得各自配各自的WaitGroup或用errgroup。
值传递 sync.WaitGroup 导致状态分裂
sync.WaitGroup 是 struct,含 mutex 和计数器字段,不可复制。
错误写法:
var wg sync.WaitGroup wg.Add(1) go worker(wg) // ❌ 值传递:内部 state 被拷贝,Done() 减的是副本计数器 wg.Wait() // 永远等不到 0
或:
w2 := wg // 隐式复制,后续 w2.Add / w2.Done 都无效
正确传参方式只有一种:
- 函数参数必须是
*sync.WaitGroup; - 切片或 map 中存 WaitGroup?不行。只能存指针:
[]*sync.WaitGroup; - 初始化后别赋值给新变量,避免无意识复制。
WaitGroup 的计数器归零 ≠ 所有 goroutine 已退出,它只认“Done() 被调了多少次”。哪怕某个 goroutine panic 了,只要 defer wg.Done() 跑了,它就算“完成”。真正要确保资源释放、连接关闭、文件写入完成,得靠业务逻辑自己兜底——WaitGroup 只管数对了没。










