← 中文资源

简体中文版本

Go WaitGroup panic — Wait 之后调用 Add / 计数为负

通过在任何 goroutine 启动前调用 Add、绝不在其中调用,并拒绝负计数,修复 sync.WaitGroup 误用导致的 panic。

阅读英文原文

症状

本篇调试指南针对“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。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。