WaitGroup.Add()必须在启动goroutine前调用,Done()须与Add()成对且在同一goroutine中执行,动态扩容时Add参数须精确匹配实际goroutine数,WaitGroup仅适用于纯同步等待场景。

WaitGroup.Add() 必须在启动 goroutine 前调用
动态协程池里最容易出错的,就是把 Add() 放到 goroutine 里执行。这会导致 Wait() 提前返回,因为主 goroutine 没等到任何实际任务完成。
根本原因:Add() 和 Done() 操作的是同一个计数器,但 Add() 调用时机决定了 WaitGroup 是否“知道”有任务在跑。如果在 goroutine 内部才调用 Add(1),那主流程可能已经执行完 Wait() 了。
- 正确做法:每次向池提交新任务前,立刻调用
wg.Add(1) - 错误写法:
go func() { wg.Add(1) // ❌ 危险!Add 在 goroutine 内部 defer wg.Done() // ... work }() - 典型现象:程序秒退、日志没打全、结果为空,但无 panic
Done() 必须与 Add() 成对且在同 goroutine 中调用
不能靠外部逻辑“代劳” Done(),也不能跨 goroutine 调用 —— WaitGroup 的计数器不是线程安全的“增减任意”,而是要求每对 Add()/Done() 严格绑定在同一 goroutine 生命周期内。
常见误用场景是“统一回收”或“中间件拦截”,比如在 pool 的 close() 方法里批量调用 Done(),这会触发 panic:panic: sync: negative WaitGroup counter。
立即学习“go语言免费学习笔记(深入)”;
-
Done()必须出现在对应 goroutine 的末尾(或 defer 中) - 若任务可能 panic,务必用
defer wg.Done(),否则计数器卡死 - 不要在 select/case 或 error 分支里漏掉
Done(),尤其当有多个 return 点时 - 示例安全写法:
wg.Add(1) go func() { defer wg.Done() // ✅ 保证执行 if err := doWork(); err != nil { log.Println(err) return // wg.Done() 仍会执行 } }()
动态扩容时 Add() 的数值必须精确匹配实际启动 goroutine 数量
协程池常按需拉起 goroutine,比如根据待处理任务数决定启多少个 worker。这时 Add(n) 的 n 必须等于你真实 go f() 的次数,不能估算、不能取 min/max、不能复用旧值。
尤其注意循环中启 goroutine 的场景:循环变量被闭包捕获导致所有 goroutine 共享同一个值,进而可能少启或多启,让 Add() 和实际 goroutine 数不一致。
- 错误模式:
for i := range tasks { wg.Add(1) go func() { // ❌ i 是共享变量 defer wg.Done() process(tasks[i]) // 可能越界或重复处理同一项 }() } - 修复方式:传参捕获当前值:
for i := range tasks { wg.Add(1) go func(task Task) { defer wg.Done() process(task) }(tasks[i]) } - 更稳妥的做法:先算总数,再
Add(total),再循环启 goroutine,避免计数漂移
WaitGroup 不适合替代 channel 控制任务生命周期
WaitGroup 只管“是否全部结束”,不管“任务是否就绪”“是否被取消”“结果如何”。在动态协程池中,如果需要等待任务完成 + 获取返回值 + 支持超时或取消,硬套 WaitGroup 会让代码越来越难维护。
比如你想等 5 个请求返回,但其中 2 个超时了,你仍要等剩下 3 个 —— 这时 Wait() 无法中断,也无法告诉你哪些完成了。而 context.Context + channel 才是更自然的组合。
- WaitGroup 适合:纯同步等待,无返回、无超时、无取消需求
- WaitGroup + channel 组合可行,但别用 WaitGroup 替代 channel 的通信语义
- 容易忽略的点:WaitGroup 零值可用,但一旦
Add()过就不能再复制;结构体字段含指针,赋值或传参时别意外复制


















