嵌套 Goroutine 中 Add() 必须在启动前调用:WaitGroup 不自动感知嵌套,Add(1) 需在 go 语句前执行;同一实例上严格配对;循环中需每次新建 wg 或用 sync.Pool 缓存;必须传指针避免浅拷贝;Done() 务必在入口 defer,防漏调或多调。

嵌套 Goroutine 中 Add() 必须在启动前调用
WaitGroup 支持嵌套,但不是“自动感知嵌套”的智能工具——它只认 Add() 和 Done() 的配对关系。子 Goroutine 内部再启新 Goroutine 时,Add(1) 仍必须出现在 go 语句之前,不能塞进新 Goroutine 里。
- 错误写法:
go func() { wg.Add(1); defer wg.Done(); ... }()→ 主 Goroutine 可能已执行Wait(),而计数器还是 0 - 正确写法:先
wg.Add(1),再go func() { defer wg.Done(); ... }() - 嵌套场景下,Add/Done 配对发生在**同一 WaitGroup 实例上**,无需新建 wg,只要确保调用时机和次数严格匹配
循环启动嵌套任务时别复用 WaitGroup 变量
如果外层是 for 循环,每轮都启动一批嵌套 Goroutine,WaitGroup 不能在 Wait() 后继续 Add() —— Go 1.20+ 会直接 panic:sync: negative WaitGroup counter。
- 错误模式:
for i := range tasks { wg.Add(1); go worker(&wg); wg.Wait() }→ 第二轮Add(1)触发 panic - 正确做法:每次循环都声明新变量,
for i := range tasks { var wg sync.WaitGroup; wg.Add(1); go worker(&wg); wg.Wait() } - 若性能敏感(如高频循环),可用
sync.Pool缓存*sync.WaitGroup,但必须确保无并发访问,且每次取用后重置(实际无法 reset,所以 Pool 更适合“按需分配 + 用完丢弃”)
嵌套层级深时务必传指针,避免浅拷贝分裂计数器
sync.WaitGroup 不可复制。函数参数传值、结构体字段赋值、append 到切片、甚至作为 map value 存储,都会触发浅拷贝,导致内部 state1 字段被复制,计数器失联。
- 错误示例:
func handle(wg sync.WaitGroup)或subWg := wg→ 两个 wg 各自维护计数器,Done()调不到主 wg 上 - 正确方式:所有跨作用域使用都传
*sync.WaitGroup,包括嵌套 Goroutine 的参数、闭包捕获、方法接收者 - 特别注意测试场景:
t.Run并发子测试中,若把 wg 闭包进子测试函数又未加锁或传指针,极易因竞态导致漏 Done 或重复 Done
动态嵌套任务中 Done() 漏调比复用更致命
嵌套越深,分支越多,Done() 被遗漏或重复调用的风险越高。漏调 → Wait() 永久阻塞;多调 → 计数变负 → panic。这不是编译错误,运行时才暴露。
立即学习“go语言免费学习笔记(深入)”;
- 常见漏点:error early return 后忘记
defer wg.Done();recover 捕获 panic 后没补Done() - 常见多调:
defer wg.Done()+ 手动wg.Done();defer 放在 if 分支内,被多次执行;闭包捕获循环变量导致多个 Goroutine 共享同一defer语句 - 硬性建议:一律在 Goroutine 函数入口第一行写
defer wg.Done(),不加条件,不嵌套,不换行
Add()/Done() 都得落在正确的 goroutine 作用域里、正确的执行顺序上。一旦混入 channel、context 或 error 处理,配对关系就容易藏在缩进深处,肉眼难验。


















