waitgroup.add必须在goroutine启动前调用,done必须由对应goroutine自行调用且成对出现,waitgroup不可复制需传指针,不适用于动态任务场景。

WaitGroup.Add 必须在 goroutine 启动前调用
如果在 goroutine 内部才调用 Add(1),Wait() 很可能已经返回或永远阻塞——因为主 goroutine 无法感知后续启动的协程,计数器初始为 0,Wait() 立即返回;而若先 Wait() 再启动协程,则因计数始终为 0 而永久阻塞。
正确做法是:每次要启一个新 goroutine 前,立刻调用 Add(1),确保计数器在 goroutine 开始执行前已更新。
- ✅ 正确:
wg.Add(1); go func() { ...; wg.Done() }() - ❌ 错误:
go func() { wg.Add(1); ...; wg.Done() }() - ⚠️ 危险:多个 goroutine 并发调用
Add()而未加锁(WaitGroup的Add不是并发安全的)
Done 必须与 Add 成对出现,且只能在对应 goroutine 中调用
Done() 是 Add(-1) 的别名,它必须由对应 goroutine 自己调用,不能由父 goroutine 或其他协程代劳。否则容易出现计数负值(panic)或提前唤醒 Wait()。
常见错误是把 Done() 放在 defer 中但位置不对,或在 error 分支遗漏调用。
- ✅ 推荐写法:
defer wg.Done()放在 goroutine 函数最开头(确保无论是否 panic 都会执行) - ❌ 错误写法:
if err != nil { return } wg.Done()—— error 时跳过Done(),计数不减,Wait()永不返回 - ⚠️ 注意:
Done()调用次数超过Add()总和会 panic:panic: sync: negative WaitGroup counter
WaitGroup 不能被复制,跨 goroutine 复用需传指针
sync.WaitGroup 是含 mutex 的结构体,直接值传递会复制内部状态(包括未同步的计数器和 mutex),导致行为不可预测:有的 goroutine 调用 Done() 对主 goroutine 的 Wait() 完全无效。
Go 编译器不会报错,但运行时表现诡异——比如 Wait() 一直阻塞,即使所有子 goroutine 已结束。
- ✅ 正确:传
*sync.WaitGroup,例如go worker(&wg, job) - ❌ 错误:
go worker(wg, job)(值传递) - ⚠️ 补充:
WaitGroup{}初始化后可直接取地址,无需额外 new;但绝不能从 map 或 struct 字段中“取值再传”
WaitGroup 不适用于动态增减任务场景
WaitGroup 设计用于已知数量的固定任务集合。若在 Wait() 执行期间又动态调用 Add()(比如 worker 启动新子任务),行为未定义:Go 文档明确指出 “Add can be called at any time before Wait”,但实际中若 Wait() 已阻塞,Add() 可能被忽略或触发竞争。
需要动态任务调度时,应改用 context + channel 或 errgroup.Group。
- ✅ 替代方案:
errgroup.Group支持在 goroutine 内部安全调用Go()启动新任务 - ❌ 强行 hack:
for range jobs { wg.Add(1); go func() { ... wg.Done() }() }只适用于 jobs 数量启动前已确定 - ⚠️ 注意:
WaitGroup无超时机制,配合time.AfterFunc或select实现超时需自行管理
最常被忽略的是:Add 和 Done 的调用时机不是“语法对就行”,而是跟 goroutine 生命周期严格绑定的同步契约。一旦错位,程序不会报编译错误,但会在高并发或特定调度路径下间歇性死锁——这种 bug 很难复现,也很难靠日志定位。











