WaitGroup 计数器只认原子值归零而非 goroutine 结束,Add 必须在 goroutine 启动前完成且不可动态追加,必须传指针、不可复用,Add/Done 需严格一一对应。

WaitGroup 的计数器不是“等 goroutine 结束”,而是等 Add/Done 配对归零
很多人卡在 wg.Wait() 不返回,第一反应是“goroutine 没结束”,但实际问题往往出在计数器没归零——它根本不关心 goroutine 是否存活,只认原子计数器值。
- 如果
wg.Add(1)漏调,或调晚于go启动,Done()就可能在计数器还是 0 时执行,导致负值 panic - 如果
defer wg.Done()写在函数中间(比如 error 分支之后),error 提前 return 就跳过Done(),计数器卡住 - goroutine panic 时
defer仍会执行,所以defer wg.Done()是安全的;但手动调用wg.Done()+defer wg.Done()就会多减一次,触发sync: negative WaitGroup counter
Add 必须在 goroutine 启动前完成,且不能动态追加
wg.Add(n) 是一次性声明“我预期有 n 个 Done”,不是注册监听器。Go runtime 不会帮你追踪后续新启的 goroutine。
- ❌ 错误:在循环里先
go func() { ... }(),再wg.Add(1)——Done()可能早于Add()执行 - ❌ 错误:在 goroutine 内部调
wg.Add(1)想“动态扩容” —— 违反 WaitGroup 轻量设计,且极易因竞态导致负计数 - ✅ 正确:批量任务用
wg.Add(len(tasks));单个任务用wg.Add(1)紧贴go语句之前 - ⚠️ 注意:传入
wg.Add(-1)或其他负值是明确禁止的,Done()就是为此存在
WaitGroup 必须传指针,值传递会导致 Wait 永久阻塞
sync.WaitGroup 内部含 mutex 字段,值传递会复制锁状态,Done() 作用于副本,原始计数器不变。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- ❌ 错误:
go worker(wg)(wg是值)→wg.Wait()永不返回 - ✅ 正确:
go worker(&wg),且函数签名必须是func worker(wg *sync.WaitGroup) - ⚠️ 闭包中直接引用外部
wg变量时,确保它可取地址:局部变量 OK,结构体嵌套字段若未导出或不可寻址(如someStruct.wg是非导出字段),&someStruct.wg会编译失败
WaitGroup 不可复用,重置需新建实例
Go 1.21+ 明确禁止在 wg.Wait() 返回后继续调 wg.Add(),会 panic:sync: WaitGroup is reused before previous Wait has returned。
立即学习“go语言免费学习笔记(深入)”;
- ❌ 危险操作:
wg = sync.WaitGroup{}—— 旧 goroutine 可能仍在访问原wg,引发竞态 - ✅ 安全做法:每次并发任务组用全新变量,如
var wg sync.WaitGroup放在函数作用域内 - ⚠️ 如果真要复用(极少见),必须确保:① 所有 goroutine 已结束且
Done()全部执行完毕;②wg.Wait()已返回;③ 无任何 goroutine 持有该wg的引用 —— 实际上几乎无法可靠验证,不如新建
最隐蔽的坑不在代码行,而在配对关系:Add 和 Done 必须严格一一对应,且顺序不可颠倒。没有编译器检查,没有运行时警告,错一次就死锁或 panic,调试时连堆栈都看不出哪条路径漏了 Done。

















