sync.WaitGroup在动态协程池中必须严格配对使用:Add须在goroutine启动前调用,Done须在同goroutine中defer执行,不可重用,超时场景需确保最终Done,否则导致静默失败或panic。

sync.WaitGroup 在动态协程池里不是“能用就行”,而是“用错就崩”。它不支持运行时重置、不能跨 goroutine 配对 Add/Done、也不容忍计数器负值——这些不是边界情况,是日常踩坑点。
WaitGroup.Add 必须在 goroutine 启动前调用
常见错误是把 wg.Add(1) 放进 goroutine 里,比如:
go func() {
wg.Add(1) // ❌ 主流程可能已执行完 wg.Wait()
defer wg.Done()
// ...
}()
现象是程序秒退、日志断在中间、结果为空,但没 panic。根本原因是 WaitGroup 的计数器只在 Add 被调用后才“感知”到任务存在;如果 Add 晚于 Wait 执行,主 goroutine 就直接往下走了。
- 正确写法:每次提交任务前立刻
wg.Add(1) - 动态扩容时,
wg.Add(n)的n必须等于你真实调用go f()的次数,不能靠 len(tasks) 估算(比如循环变量闭包捕获导致实际启动数 ≠ 预期) - 若任务数为 0,别调
wg.Add(0)—— 虽然语法合法,但容易掩盖逻辑疏漏
Done 必须与 Add 成对且在同 goroutine 中执行
Done 不是“统一回收”或“批量清理”的接口,它是每个 goroutine 生命周期的收尾动作。跨 goroutine 调用 wg.Done() 或在 pool.Close() 里集中调用,会触发 panic: sync: negative WaitGroup counter。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
defer wg.Done(),尤其当函数有多个return点或可能 panic 时 - 不要在
select的某个case里漏掉Done,例如 error 分支提前返回却没调Done - 禁止在 defer 里再起 goroutine 并调
Done—— defer 是同步执行的,那层 goroutine 无法保证执行时机
WaitGroup 无法复用,动态池需配合重置逻辑
WaitGroup 计数器归零后不可再次 Wait,官方明确不支持重用。动态协程池反复启停时,常见误操作是声明一个全局 var wg sync.WaitGroup 然后反复 Wait。
- 方案一:每次任务批次新建局部
wg(推荐),避免状态残留 - 方案二:用
sync.Pool缓存*sync.WaitGroup实例,但需确保取用后重置计数器(实际不可行,因无公开重置 API) - 方案三:改用
chan struct{}+close+range模式替代纯等待场景,尤其当协程还向共享 channel 发送结果时
协程池中 WaitGroup 与超时/取消的协作难点
单纯 wg.Wait() 是阻塞到底,没有超时机制。加 time.After 或 context.WithTimeout 时,容易忽略:即使超时返回,goroutine 仍在后台跑,Done 还没被调用,计数器卡住。
- 超时后不能直接丢弃
wg,得确保所有已启动 goroutine 最终都执行了defer wg.Done() - 若任务本身支持 cancel(如传入
ctx),应在ctx.Done()分支里仍调wg.Done() - 避免在超时分支里手动
wg.Add(-1)补偿 —— 这破坏配对原则,且线程不安全
真正难的不是写对第一版 wg.Add/Done,而是在任务拆分、错误分支、循环启停、超时中断这些交织逻辑里,始终守住“每启一个,必减一个,且减在同一个 goroutine 里”这条线。稍一松懈,就是静默失败或 panic。


















