waitgroup.add()传负数会立刻panic,done()调用超次或复用waitgroup也会panic,add()必须在goroutine启动前调用,且不可观察计数器值。

WaitGroup.Add() 传负数会立刻 panic
只要 Add() 参数是负数,Go 运行时就在调用那一瞬间炸,不等 Wait() 执行,也不管当前计数是不是 0。这不是状态异常,而是硬性拦截——因为内部用 uint64 存计数器,负值会绕回极大正数导致死锁,所以直接 panic("sync: negative waitgroup counter")。
常见写法踩坑:
- 把
Done()误写成Add(-1),又在别处再调一次Done() - 循环里写
wg.Add(len(tasks) - i),但i超出范围(比如i == len(tasks)+1) - 并发读写一个变量后传给
Add(),比如delta := count--; wg.Add(delta),count被多个 goroutine 修改,delta算出来是负的
Done() 调用次数超过 Add() 总和也会 panic
Done() 就是 Add(-1) 的封装,它不检查当前计数是否 > 0,只做原子减法。一旦减到 -1,立刻 panic。
典型场景:
- 函数里写了
defer wg.Done(),但前面有多个return分支,其中某个分支已经手动调过wg.Done() - goroutine 启动失败(比如
if err != nil { return }),但defer wg.Done()仍会执行,而主流程又在别处补了一次Done() - 闭包捕获循环变量,导致部分 goroutine 根本没启动,但
Done()却被所有迭代都触发了(比如for _, item := range items { go func() { defer wg.Done(); work(item) }() })
WaitGroup 不可复用,复用等于埋雷
文档没明说“禁止复用”,但行为上它不支持:一旦 Wait() 返回,计数器归零;此时再 Add() 是允许的,但危险在于——上一轮还没结束的 goroutine 可能还在跑,它们的 Done() 会作用到新一轮的计数上,造成错乱。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
更隐蔽的是跨任务干扰:
- 把同一个
*sync.WaitGroup当结构体字段反复用,没控制生命周期 - HTTP 优雅关闭中,
net.Conn.Close()可被多次调用(标准库、reverse proxy、超时逻辑都会触发),如果每次关都无条件wg.Done(),必然负值 - 修复方案必须用指针 +
sync.Once,例如:closed sync.Once; func (w *gracefulConn) Close() { w.closed.Do(func() { wg.Done() }) }
Add() 必须在 go 语句之前调用
这是最常被忽略的顺序约束。Add() 不是“发现 goroutine 就加一”,而是“预登记待等待数量”。如果写成 go func() { wg.Add(1); defer wg.Done() }(),就变成 goroutine 内部加一,和主 goroutine 的 Wait() 构成竞态——Wait() 可能提前返回,也可能永远卡住。
安全写法只有一种:
-
wg.Add(1)→go func() { defer wg.Done(); ... }() - 批量启动时,每个
go前都配一个wg.Add(1),不要先wg.Add(len(tasks))再统一启 goroutine,除非你 100% 确保循环一定执行完 - 闭包传参要显式绑定:用
go func(item Item) { defer wg.Done(); process(item) }(item),而不是捕获item变量
计数器本身不可观察,任何试图打印当前值的调试操作都会引入竞态。真要定位问题,只能靠日志打在 Add() 和 Done() 前后,或用 go tool trace 看 goroutine 时序。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










