waitgroup.add 必须在 goroutine 启动前调用,否则因 add 与 done 不配对或并发调用导致 wait 永不返回或 panic;add(1) 需每次 go 前执行,不可提前提成 add(n);done 应 defer 调用,但需避免闭包捕获变量错误。

WaitGroup.Add 必须在 goroutine 启动前调用
常见错误是把 Add 放进 goroutine 里,比如写成:
go func() {
wg.Add(1) // ❌ 错误:Add 和 Done 不在同一个逻辑上下文,可能漏加、多加或竞态
defer wg.Done()
// ...
}()
这会导致 Wait 永远不返回,或 panic:「negative WaitGroup counter」。因为 Add 和 Done 不配对,且 Add 被并发调用时没加锁保护(WaitGroup 自身不保证 Add 的并发安全)。
-
Add应始终在go语句之前,明确表达“我即将启动一个要等待的任务” - 循环启动多个协程时,
Add(1)必须在每次go前执行,不能提前提成Add(n)—— 除非你能确保后续一定启动 n 个,否则一旦中间出错(如条件跳过),计数就失准 - 如果任务数动态决定(比如从 channel 读取),建议先 collect 列表再批量
Add,避免边读边启造成的计数漂移
Done 要用 defer,但注意闭包变量捕获陷阱
写法 defer wg.Done() 是标准姿势,但容易和闭包一起埋雷。典型反例:
for i := 0; i <p>必须显式传参隔离:</p> <pre class="brush:php;toolbar:false;">for i := 0; i
- 不传参 + 匿名函数引用循环变量 = 竞态根源之一
- 即使加了
defer wg.Done(),若业务 panic 未被捕获,Done就不会执行 → 计数卡住 →Wait死锁 - 必要时在 goroutine 内加 recover,确保
Done总被执行
WaitGroup 不可重用,重复 Wait 会 panic
WaitGroup 在计数器归零后进入终态,再次调用 Add 或 Wait 会 panic:「sync: WaitGroup is reused before previous Wait has returned」。
常见于想复用同一 var wg sync.WaitGroup 处理多轮任务的场景。例如:
for round := 0; round
- 没有「重置」方法,也不能靠赋值
wg = sync.WaitGroup{}—— 这是复制结构体,触发noCopy检查(Go vet 会报错,运行时也可能 panic) - 正确做法:每轮新建局部变量,如
var wg sync.WaitGroup放在循环内 - 若需长期持有,改用指针传参 + 函数封装,确保每次调用都初始化新实例
WaitGroup 无法超时,必须配合 context 控制等待上限
WaitGroup.Wait() 是纯阻塞,无超时机制。一旦某个 goroutine 漏掉 Done 或卡死,主协程就永久挂起。
工程中必须兜底:
- 用
context.WithTimeout包一层,select等待Wait完成或超时 - 不要自己起 goroutine 调
Wait再用 timer,那样多一层调度开销且难管理 - 示例关键片段:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case
<p>真正容易被忽略的是:超时后 <code>WaitGroup</code> 计数器未归零,它已处于无效状态,后续不能再调 <code>Wait</code> 或 <code>Add</code> —— 这不是 bug,是设计使然。超时即放弃,别试图“抢救”这个 <code>WaitGroup</code> 实例。</p>golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











