WaitGroup.Add() 必须在 goroutine 启动前调用,否则易因 Done() 先执行导致计数器为负而 panic;正确顺序是先 Add(1),再启动 goroutine 并 defer Done()。

WaitGroup.Add() 必须在 go 语句之前调用
这是最常触发 panic: sync: negative WaitGroup counter 的地方。Add 的作用是“预告任务数”,不是“补登记”。一旦 goroutine 已启动,它可能在 Add(1) 执行前就完成并调了 Done(),计数器瞬间变负。
常见错误写法:
-
go func() { wg.Add(1); defer wg.Done(); ... }()—— Add 在 goroutine 内部,完全不可靠 -
go func() { defer wg.Done(); ... }(); wg.Add(1)—— 主 goroutine 可能已执行到wg.Wait(),此时计数仍为 0
正确做法只有一种:先 wg.Add(1),再 go func() { defer wg.Done(); ... }()。循环启动多个时,也必须每轮都严格遵循这个顺序:
for i := 0; i < n; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// work...
}(i)
}
Done() 必须用 defer 且放在 goroutine 函数开头
Done() 是减 1 操作,漏调 → wg.Wait() 永久阻塞;多调 → 计数器负值 → panic。任何提前 return(error 分支、panic、recover 后未处理)都可能跳过裸写的 wg.Done()。
立即学习“go语言免费学习笔记(深入)”;
所以必须用 defer wg.Done(),而且要放在 goroutine 入口处,不能包在 if 或子函数里:
- ✅ 正确:
go func() { defer wg.Done(); if err != nil { return }; doWork() } - ❌ 错误:
go func() { if ok { wg.Done() } }—— 条件不满足时就漏了 - ⚠️ 高危:
go func() { defer wg.Done(); f(); defer wg.Done() }—— 重复 defer 导致 double Done
注意:defer wg.Done() 不等于“最后才执行”,而是注册一个退出时必触发的操作,包括 panic 场景。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
WaitGroup 实例必须传指针,禁止值传递
sync.WaitGroup 内部含 mutex 和原子计数器,值传递会复制整个结构体,导致子 goroutine 调的 Done() 作用于副本,主 goroutine 的 wg.Wait() 看不到变化,最终死锁。
典型错误:
- 函数签名写
func worker(wg sync.WaitGroup),调用传wg(值) - struct 字段直接嵌套
sync.WaitGroup,然后传 struct 值
正确方式只有一种:函数参数声明为 wg *sync.WaitGroup,调用时传 &wg。即使函数只调 Done(),也必须是指针。
Wait() 返回后不能再调 Add() 或 Done()
wg.Wait() 返回只表示当前计数器归零,不代表 WaitGroup “重置”或“可复用”。Go 1.21+ 明确禁止在 Wait() 返回后继续调 Add(),会 panic:sync: WaitGroup is reused before previous Wait has returned。
如果你需要多次等待不同批次任务:
- ❌ 不要循环里反复
wg.Add()+go+wg.Wait() - ✅ 每次新建变量:
var wg sync.WaitGroup,或从sync.Pool获取指针实例(需确保无并发访问) - ✅ 更推荐场景化替代:动态任务用
errgroup.Group,带超时用context.WithTimeout配合select
真正容易被忽略的是生命周期——WaitGroup 实例必须存活到所有 Done() 完成,不能是短命局部变量而 goroutine 异步执行太久。

















