waitgroup 不能安全复用,go 1.21+ 在 wait() 返回后调用 add() 会 panic;必须每次循环声明新变量,传指针并确保 goroutine 操作同一实例,否则 done() 失效或计数器分裂。

WaitGroup 不能安全复用,所谓“重置”是错觉;每次循环必须声明新变量,否则 Go 1.21+ 会 panic。
WaitGroup.Wait() 返回后调用 Add() 会 panic
Go 1.21 起明确禁止在 Wait() 返回后对同一实例再次调用 Add(),运行时直接报错:panic: sync: WaitGroup is reused before previous Wait has returned。这不是竞态问题,而是设计上拒绝未定义行为。
- 即使你手动写
wg = sync.WaitGroup{},旧的wg可能还在被 goroutine 访问,造成计数器分裂或死锁 - 用
reflect.Zero(reflect.TypeOf(wg)).Interface().(sync.WaitGroup)之类方式“重置”也不行——sync.WaitGroup内部含noCopy字段,编译期 vet 会警告,运行时可能静默失效 - 别信“零值等价可重用”的说法:那个
wg1 == wg2示例只在无并发、无 goroutine 引用前提下成立,实际工程中毫无意义
循环中启动多轮任务的正确写法
每轮任务都该用独立的 sync.WaitGroup 实例,不共享、不传递、不复用。
- 在 for 循环体内声明:
var wg sync.WaitGroup,而不是在循环外声明一个再反复“清空” - 如果需要跨函数协作(比如分发任务 + 收集结果),把
*sync.WaitGroup作为参数传入,但确保每次调用都对应新实例 - 避免嵌套循环里误用外层
wg:例如外层遍历服务、内层遍历请求,两层都要各自Add()和Wait()
示例:
for _, svc := range services {
var wg sync.WaitGroup
for _, req := range svc.requests {
wg.Add(1)
go func(r request) {
defer wg.Done()
process(r)
}(req)
}
wg.Wait()
}
为什么传指针 + 每次新建才是唯一安全路径
sync.WaitGroup 内部含互斥锁和条件变量,值传递会复制锁状态,导致 Done() 作用于副本,主 Wait() 永远收不到信号。而复用意味着多个 goroutine 可能同时操作同一个计数器,哪怕你加了锁也挡不住底层 noCopy 的 runtime 检查。
- 结构体字段赋值、
append到切片、map存储都会触发浅拷贝 →Done()失效 - 测试中用
t.Run并发子测试时,若闭包捕获外层wg且未传指针,每个子测试看到的其实是不同副本 - 真正难的不是语法,是当代码混着
context.WithTimeout、select、error return、recover 时,Add/Done 配对关系是否还能一眼看清
最易被忽略的一点:WaitGroup 不提供 cancel 能力,也不感知 context 超时。它只忠实地数到零——哪怕所有 goroutine 都卡死、panic 或被取消,只要没调 Done(),Wait() 就永远挂住。别指望靠“复用”绕过这个问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











