症状
本篇调试指南针对“Go WaitGroup panic — Wait 之后调用 Add / 计数为负”。内容涵盖可观察的症状、可复现的诊断路径、根本原因、具体修复方式,以及如何验证修复并防止回归。
可观察到的症状包括:
- panic: sync: WaitGroup is reused before previous Wait has returned
- panic: sync: negative WaitGroup counter
- race 检测器报告 WaitGroup 误用警告
诊断
先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。
步骤 1
go test -race ./...
race 检测器会标记 Add-after-Wait 与并发 Add/Wait,并指出确切行。
步骤 2
grep -rn "wg.Add\|wg.Done" .
每个 Add 必须在其追踪的 goroutine 启动前执行;Done 应位于 goroutine 内部。
常见诱因(按出现频率排序):
- wg.Add 在派生出的 goroutine 内部调用,而非启动前
- wg.Done 调用次数多于 wg.Add
- 第一批尚未 Wait 返回就复用同一 WaitGroup
根因
WaitGroup 要求在被追踪的 goroutine 启动前、Wait 之前完成正的 Add。与 Wait 并发调用 Add,或在计数归零后调用,都会与内部状态竞争并抛出复用或负计数 panic。
修复
一次性 wg.Add(n) 再启动 n 个 goroutine,在每个 goroutine 顶部 defer wg.Done(),每批使用新的 WaitGroup 或用 errgroup 替代复用。
注意:按值复制 WaitGroup 也是缺陷;应始终以指针传递或嵌入结构体。
var wg sync.WaitGroup
wg.Add(len(urls)) // BEFORE launching
for _, u := range urls {
u := u
go func() {
defer wg.Done() // inside the goroutine
fetch(u)
}()
}
wg.Wait()
// then a fresh WaitGroup for the next batch — never reuse a waited one.
wg = sync.WaitGroup{}
验证
用 -race 与 -count=100 重跑,确认无 panic 无警告;断言完成的任务数等于 n。
修复后补充一条回归测试,复现原始症状,防止问题再次出现。
这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。