WaitGroup.Add() 传负数会 panic,因内部做显式检查防止计数器溢出;常见错误包括提前 Done()、重复调用 Done()、复用未等待完成的 WaitGroup。
WaitGroup.Add() 传负数会 panic
go 运行时明确禁止对 sync.waitgroup 调用 add(-1) 或任何负值——这会直接触发 panic: sync: negative waitgroup counter。不是警告,不是返回错误,而是崩溃。根本原因在于 waitgroup 内部用一个无符号整数(uint64)存储计数器,但 add 方法做了显式负数检查并 panic,防止溢出后绕回成极大正数导致死锁。
常见诱因是:在 goroutine 启动前没调用 Add(1),或在多个地方重复调用 Done(),又或者误把 Done() 当作“减去任意值”来用。
- 永远只在启动 goroutine 之前、且仅一次调用
Add(1) - 不要在循环体外预设总数量再循环里
Add(n),除非你 100% 确保循环一定会执行 n 次(比如固定数组遍历) - 避免在 defer 中无条件调用
Done(),尤其当函数可能提前 return 且已调过Done()
Done() 调用次数多于 Add() 总和
这是最隐蔽的负数来源:Done() 本质就是 Add(-1),只要调用次数超过所有 Add(n) 的总和,下一次 Done() 就 panic。典型场景是错误处理分支里多写了一次 Done(),或 goroutine 启动失败却仍执行了 Done()。
例如:
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
go func() {
defer wg.Done() // ✅ 正常情况
if err := process(job); err != nil {
log.Println(err)
wg.Done() // ❌ 错误:这里又调了一次
return
}
}()
}
修复方式很简单:删掉手动 Done(),只保留 defer 那一次;或者改用显式控制流,确保 Done() 只执行一次。
- 优先用
defer wg.Done(),放在 goroutine 函数开头,而不是结尾 - 如果逻辑复杂需提前退出,把
Done()放到所有 return 路径之后(用 goto 或封装 helper 函数) - 调试时可在
Done()前加日志,如log.Printf("wg.Done() called, remaining: %d", wgCounter)—— 注意:无法直接读取内部计数,需自己维护一个原子变量辅助排查
WaitGroup 复用时未重置计数器
sync.WaitGroup 不支持 Reset() 方法。一旦 Wait() 返回,内部计数器归零,但若再次调用 Add(),它会从 0 开始累加——这本身合法。问题出在「复用未清理的 WaitGroup 实例」:比如某个 goroutine 还没结束就重新 Add(),或上一轮还没 Wait() 完就开启下一轮。
典型错误:
AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() { defer wg.Done(); doWork() }()
}
wg.Wait() // 第一轮
// 紧接着复用
wg.Add(1) // ❌ panic!此时 wg 内部计数为 0,Add(1) 合法,但...
go func() { defer wg.Done(); doMore() }()
wg.Wait() // ❌ 如果上一轮 goroutine 还没全结束,这里可能 panic 或行为未定义
真正安全的做法是:每个逻辑周期用新的 sync.WaitGroup 实例,或确保上一轮彻底结束(Wait() 返回)后再开始下一轮,且不混用。
- 不要把
sync.WaitGroup声明为包级变量长期复用 - 在函数内声明局部
var wg sync.WaitGroup,让它随作用域自然消亡 - 若必须跨函数传递,确保接收方只负责
Add()和Done(),Wait()必须由发起方统一调用
替代方案:用 channels + sync.WaitGroup 组合更可控
单纯靠 WaitGroup 很难避免计数错配,尤其在有错误分支、重试、取消的场景。推荐用 channel 控制生命周期,WaitGroup 仅用于等待 goroutine 退出本身。
例如:
done := make(chan struct{})
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
go func(j Job) {
defer wg.Done()
select {
case <-done:
return
default:
process(j)
}
}(job)
}
close(done) // 通知全部退出
wg.Wait()
这样 Done() 调用完全与业务逻辑解耦,只反映 goroutine 是否已退出,不再承担“任务完成”的语义。
真正容易被忽略的是:WaitGroup 的设计初衷只是同步 goroutine 生命周期,不是任务状态管理器。把它当成任务计数器用,迟早踩坑。

















