waitgroup需严格遵循“先add后go、done必defer”原则:add必须在goroutine启动前调用以预告任务数,done必须在goroutine内用defer确保执行,否则易致负计数panic或死锁。

WaitGroup 不是“等完就完”的工具,它是一把双刃剑:用对了省事,错一步就 panic 或死锁。
wg.Add 必须在 go 语句之前调用
这是最常触发 panic: sync: negative WaitGroup counter 的地方。Add 的作用是“预告任务数”,不是“记录已完成”。一旦 goroutine 启动后才调 wg.Add(1),它可能在 Add 执行前就跑完并调了 wg.Done(),计数器瞬间变负。
- ✅ 正确顺序:
wg.Add(1)→go func() { defer wg.Done() }() - ❌ 错误写法:先
go func() { wg.Add(1); ... }(),或把wg.Add(1)放在循环体末尾 - 动态任务数(比如从 channel 拉取)必须先 collect 到切片,再
wg.Add(len(tasks));不能边拉边 Add
Done 必须用 defer,且只能在 goroutine 内部调用
wg.Done() 是减 1 操作,漏调 → wg.Wait() 永远阻塞;多调 → 计数器负值 → panic。defer 是覆盖所有 return 路径的唯一稳妥方式。
- ✅ 推荐写法:
go func() { defer wg.Done(); doWork(); if err != nil { return }; resultChan - ❌ 危险操作:在 error 分支里手动调
wg.Done()却忘了 defer,或在 recover 里漏掉 - ⚠️ 注意:不能由主 goroutine 代劳调
wg.Done()—— 它必须和对应任务生命周期绑定在同一个 goroutine 里
WaitGroup 必须传指针,严禁值传递
sync.WaitGroup 内部含原子字段和 mutex,值传递会复制其内存布局,导致状态错乱。常见症状是 wg.Wait() 卡住、或报 WaitGroup is reused before previous Wait has returned。
- ✅ 函数参数声明为
wg *sync.WaitGroup,调用时传&wg - ❌ 不要写
go work(wg)(值传),也不要作为 struct 字段直接赋值拷贝 - 闭包中若引用外部
wg,确保它是可取地址的变量(如局部var wg sync.WaitGroup),而非临时计算值
WaitGroup 只回答“是否做完”,不负责“做得对不对”
它不传错误、不收结果、不支持超时、不响应 cancel。硬靠它做复杂协调,等于自己造轮子。
- 要收错误?配
errChan := make(chan error, N),并在 goroutine 里errChan ;<code>wg.Wait()后再close(errChan) - 要超时?必须用
context.WithTimeout控制业务逻辑,goroutine 内监听ctx.Done()主动退出,再wg.Wait() - 要任意失败即终止?别硬刚,换
errgroup.Group—— 它底层仍是 WaitGroup,但封装了 cancel、错误聚合和上下文传播
真正容易被忽略的是:WaitGroup 零值安全,但它的“不可复制性”不是靠文档提醒,而是靠运行时 panic 强制约束。只要有一次值传递、一次 Add/Done 顺序颠倒、一次漏 defer,程序就可能在线上静默卡死或崩溃——而这些问题往往压测不出,只在特定并发节奏下暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











