WaitGroup计数需手动配对Add与Done:Add必须在go前调用以预登记任务数,Done次数须严格等于Add总和,不可复制WaitGroup(须传指针),且不提供超时或错误传递能力。

WaitGroup 计数不是“自动感知协程数量”,而是靠你手动配对 Add 和 Done —— 漏一次、多一次、顺序错,程序就卡住或 panic。
为什么 WaitGroup.Add 必须在 go 语句之前调用
现象:程序偶尔直接退出,或 Wait() 永远不返回,甚至 panic “negative WaitGroup counter”。
-
Add的作用是预登记“待等待任务数”,不是“发现一个协程就加一” - 如果写成
go func() { wg.Add(1); defer wg.Done() }(),Add就发生在协程内部,和主协程的Wait()构成竞态 ——Wait()可能提前看到 0 而返回,也可能永远等不到减法 - 正确顺序只有一种:
wg.Add(1)→go ...→ 协程内defer wg.Done() - 循环启动时,每一组
Add(1)和go必须一一对应,不能合并成wg.Add(len(tasks))后再启全部(除非你确定所有任务都已确定且不会动态增删)
Done 调用次数必须严格等于 Add 总和
现象:程序 hang 在 Wait();或 panic “sync: negative WaitGroup counter”。
-
Done()等价于Add(-1),底层是原子减法,负值立即触发 panic - 分支逻辑中 early return 容易漏掉
defer wg.Done(),比如if err != nil { return err }前没 defer,后续Done()就永远不会执行 - 切忌手动调一次 +
defer再调一次,重复减 1 会导致负计数 - 闭包捕获循环变量(如
for i := range items)时,多个 goroutine 可能共享同一个i,导致部分Done()被跳过或错位 —— 解法是显式传参:go func(item Item) { defer wg.Done(); process(item) }(item)
WaitGroup 不可复制,传参必须用指针
现象:测试里并发子测试(t.Run)中 WaitGroup 表现异常,有时 wait 立即返回,有时死锁。
立即学习“go语言免费学习笔记(深入)”;
-
sync.WaitGroup内部含原子字段(如state1),值拷贝(传参、结构体赋值、append到切片)会复制该字段,造成计数器分裂 - 错误写法:
func handle(wg sync.WaitGroup)、subWg := wg、handlers = append(handlers, wg) - 正确写法:声明用
var wg sync.WaitGroup,跨函数传参一律用&wg,所有 goroutine 中也操作*sync.WaitGroup - 尤其注意 HTTP handler 或长生命周期对象中,绝不能把
WaitGroup作为 struct 字段值拷贝存储
WaitGroup 不是 channel,也不带超时或错误传递能力
现象:以为 Wait() 返回就代表任务成功,结果错误被吞;或想等 5 秒超时,但程序卡死不动。
-
WaitGroup只回答“是否全做完”,不回答“做得对不对”——它不持有任何业务数据 - 要收集错误,得额外配
chan error或[]error+sync.Mutex,不能依赖WaitGroup - 要控制超时,必须用
select+time.After或context.WithTimeout包裹Wait(),WaitGroup本身无 context 支持 - 别在
Done()后写共享资源(如往结果chan发送),除非已加锁或用其他同步机制保护,否则存在竞态
真正难的不是写对三行代码,而是在嵌套了 channel、context、error 处理、重试逻辑的函数里,还能一眼看出每个 Add 是否都有对应 Done、是否都在正确位置。计数失配不会报错,它只在运行时让程序静默挂起或突然崩掉。


















