WaitGroup.Add()传入负数会立即panic,Done()超调或复用不当也会panic;需避免负参数、确保Add/ Done配对、禁止单例跨任务共享,并通过断言和日志预防。

WaitGroup.Add() 传入负数会立即 panic
只要 Add() 的参数是负数,Go 运行时就会在调用那一刻直接触发 panic,不等 Wait() 执行。这不是延迟报错,而是防御性检查——因为计数器不允许为负。
常见诱因包括:
- 误把
Done()写成Add(-1)后又额外调用一次Done() - 循环中用索引计算偏移量出错,比如
wg.Add(len(tasks) - i)在i > len(tasks)时结果为负 - 并发修改同一个
int变量后传给Add(),导致中间值为负(虽然WaitGroup本身线程安全,但你的参数生成逻辑未必)
示例:
var wg sync.WaitGroup wg.Add(-1) // panic: sync: negative WaitGroup counter
Done() 被调用次数超过 Add() 总和也会 panic
Done() 本质是 Add(-1),所以它每次调用都会尝试减 1。一旦当前计数器已经是 0,再调用 Done() 就等于让计数器变成 -1,立刻 panic。
典型场景有:
- 多个 goroutine 都执行了相同的清理逻辑,且都无条件调用
Done() - 错误地在
defer中写defer wg.Done(),但函数可能被多次 return 或 panic 导致Done()多次执行 - 任务提前退出(如 error 分支)后仍走到
Done(),而主流程里也有一处Done()
示例:
var wg sync.WaitGroup wg.Add(1) wg.Done() wg.Done() // panic: sync: negative WaitGroup counter
WaitGroup 复用时未重置计数器的隐性风险
sync.WaitGroup 不支持复用:一旦 Wait() 返回,内部计数器归零,但此时若再次调用 Add() 是合法的;真正危险的是「在 Wait() 尚未返回时,另一个 goroutine 提前调用了 Done()」——这往往源于逻辑竞态,而非复用本身。
AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。
更隐蔽的问题是:把同一个 WaitGroup 实例传给多个不相关的任务组,某组提前完成并 Done() 干扰了另一组的计数。这不是文档禁止的行为,但会让调试变得困难。
建议做法:
- 每个任务批次新建独立的
sync.WaitGroup实例 - 避免跨函数传递
*sync.WaitGroup,除非明确知道生命周期边界 - 不要依赖 “
Wait()返回后还能安全Add()” 来做循环任务,容易漏掉某轮的Done()
调试负值 panic 的关键线索
panic 信息固定为 sync: negative WaitGroup counter,但它不告诉你哪一行调用了 Add(-n) 或多余的 Done()。这时候得靠运行时信息定位:
- 开启
GODEBUG=asyncpreemptoff=1(仅调试用)可让栈更稳定,便于捕获准确调用点 - 在测试中加
defer func() { if r := recover(); r != nil { debug.PrintStack() } }()捕获 panic 栈 - 对高频调用
Done()的地方加日志,例如:log.Printf("done, counter=%d", unsafe.Sizeof(wg))不行,得自己封装一层带计数快照的 wrapper
最有效的预防方式,是在所有 Add() 前加断言:if n —— 虽然冗余,但能第一时间暴露逻辑错误。
负值 panic 看似简单,但根源几乎总是控制流没理清,而不是 WaitGroup 本身难用。盯住「谁在什么时候加、谁在什么时候减、加减是否一一对应」这三件事,比记 API 更管用。










