sync.WaitGroup 必须传指针、Add与Done严格配对、禁止复制、不处理错误。其内部含状态字段,值传递导致Done无效;Add需在goroutine启动前调用;结构含noCopy字段,复制会引发panic或等待失败;错误收集需配合channel等机制。

sync.WaitGroup 是 Go 里等待多个 goroutine 结束最直接、最轻量的方式——前提是只关心“是否完成”,不依赖返回值或顺序。
WaitGroup 必须传指针,不能值传递
sync.WaitGroup 内部有状态字段(比如计数器和等待者数量),值传递会复制一份独立的结构体,子 goroutine 调用 Done() 只会影响副本,主 goroutine 的 Wait() 永远等不到计数归零,最终死锁或 panic。
常见错误写法:
go worker(wg) // wg 是值,子 goroutine 修改的是拷贝
正确做法始终是传指针:
立即学习“go语言免费学习笔记(深入)”;
go worker(&wg)- 或者在 goroutine 内部显式用
&wg,比如go func() { defer wg.Done(); ... }()(此时wg是外层变量,闭包捕获的是引用)
Add 和 Done 必须严格配对,且 Add 要在 goroutine 启动前调用
Add(n) 是预设待等待数量,Done() 是事后减一。如果 Add(1) 后启动 3 个 goroutine,但只有 1 个调用 Done(),Wait() 就永远阻塞;如果 Done() 调用次数超过 Add() 总和,会 panic:panic: sync: negative WaitGroup counter。
安全写法要点:
- 循环启动 goroutine 时,每次
wg.Add(1)放在go前面 - 每个 goroutine 入口立刻用
defer wg.Done(),确保即使 panic 也能触发减计数 - 避免在循环外一次性
Add(n)后再批量启 goroutine——除非你能 100% 确保所有 goroutine 都能成功创建(比如没被 runtime 拒绝)
WaitGroup 不能复制,初始化后禁止赋值或作为函数参数值传入
sync.WaitGroup 结构体包含 noCopy 字段,Go vet 和运行时会检测复制行为。一旦发生复制(例如 tmp := wg 或 func(wg sync.WaitGroup)),程序可能在 debug 模式下直接报错,或在生产环境出现不可预测的等待失败。
典型踩坑场景:
- 把
WaitGroup嵌入 struct 后,用 struct 值方法调用(隐式复制整个 struct) - 在 for 循环中用
range遍历切片并把wg传进闭包,但闭包捕获的是循环变量的地址,而该变量在每次迭代中被复用——容易导致Add/Done错位 - 把
WaitGroup当作 map 的 value 存储(map value 是复制语义)
WaitGroup 不处理错误,需要配合 channel 或其他机制
WaitGroup 只管“有没有做完”,不管“做没做成功”。如果你要收集错误、中断失败任务、或按结果聚合,必须额外引入 chan error、sync.Once 或上下文取消。
简单错误收集模式:
errCh := make(chan error, len(files))
for _, f := range files {
wg.Add(1)
go func(file string) {
defer wg.Done()
if err := process(file); err != nil {
errCh <- err
}
}(f)
}
wg.Wait()
close(errCh)
// 后续从 errCh 读取错误注意点:
- channel 容量设为
len(files)避免发送阻塞(goroutine 卡在errCh 会导致 <code>Done()不执行) - 必须
close(errCh)后再遍历,否则range会永远等待 - 如果只要一个错误就退出,需加
context.WithCancel控制剩余 goroutine
WaitGroup 的本质就是个带原子操作的计数器 + 等待队列,它不提供超时、取消、结果传递能力。真正复杂的协调逻辑,终究要靠 context 和 channel 组合实现。


















