sync.WaitGroup 必须传指针,值传递会导致 defer wg.Done() 作用于副本而死锁;Add 必须在 goroutine 启动前调用;panic 会跳过 defer Done,需 recover 或闭包包装;重用需确保 Wait 已返回。

sync.WaitGroup 值传递导致死锁
直接把 sync.WaitGroup 当作普通参数传给函数,是引发 fatal error: all goroutines are asleep - deadlock 的最常见原因。结构体值传递会复制整个 sync.WaitGroup 实例,包括内部计数器和状态字段,而 defer wg.Done() 操作的是副本,主 goroutine 的 wg.Wait() 永远等不到计数器归零。
正确做法只有一种:传指针。
- 函数签名必须是
func worker(wg *sync.WaitGroup),调用时写worker(&wg) - 绝不能写
func worker(wg sync.WaitGroup),哪怕只是读取状态也不行(noCopy字段会在 vet 阶段报错) - 如果多个函数都要操作同一个
wg,全部统一用指针,不要混用
wg.Add() 在 goroutine 内部调用引发 panic 或提前返回
wg.Add(1) 必须在 go 语句之前执行,否则会出现竞争:wg.Wait() 可能已返回,而部分 goroutine 还没来得及 Add;或者 Done() 先于 Add 执行,触发 panic: sync: negative WaitGroup counter。
典型错误场景是循环启动 goroutine 时漏掉预加:
立即学习“go语言免费学习笔记(深入)”;
- 错:
for _, f := range files { go func() { wg.Add(1); defer wg.Done(); process(f) }() } - 对:
for _, f := range files { wg.Add(1); go func(file string) { defer wg.Done(); process(file) }(f) } - 动态任务数(如从 channel 读取)需先缓冲再批量
wg.Add(n),不能边读边Add
defer wg.Done() 在 panic 时失效
defer 只在函数正常 return 时执行。goroutine 中一旦发生未捕获 panic(比如 JSON 解析失败、空指针解引用),defer wg.Done() 就不会运行,导致 wg.Wait() 永久阻塞。
这不是“忘记写”,而是 panic 路径绕过了 defer。
- 简单修复:用
defer func() { wg.Done() }()包一层,但依然不保险 - 更稳妥:在关键逻辑外加
recover,确保wg.Done()总被执行 - 极端场景(如 HTTP handler)建议用
context+ channel 组合替代纯WaitGroup,避免单点卡死
重用 WaitGroup 未等上一轮 Wait 完成就 Add 新任务
sync.WaitGroup 可重用,但必须满足前提:上一轮 wg.Wait() 已返回。如果在 wg.Wait() 还没结束时就调用 wg.Add(),计数器可能被并发修改,行为未定义——轻则逻辑错乱,重则 panic。
实际项目中容易忽略这点,尤其在循环复用或封装成工具函数时。
- 安全重用模式:每次使用前确保
wg处于“空闲态”,即刚完成wg.Wait()且无 pending goroutine - 不要在
wg.Wait()返回前启动下一批任务 - 若需流水线式调度,改用
sync.Pool管理独立的sync.WaitGroup实例,而非复用同一个
真正难的不是记住这些规则,而是当程序卡在 wg.Wait() 时,你得快速判断是哪个 goroutine 没调 Done、是不是传了值、还是 Add 和 Done 根本没配对——这时候看 goroutine stack trace 比翻文档管用。


















