waitgroup仅用于计数与等待,不管理goroutine生命周期;add必须在go前调用,避免负计数panic;defer done易遗漏,建议显式调用;waitgroup不可复制,须传指针;它不处理错误或超时,需配合channel和select实现。

WaitGroup 不是用来“管理” goroutine 生命周期的,它只负责计数与等待;用错位置或漏调 Done,程序不是卡死就是 panic。
wg.Add() 必须在 go 语句之前调用
这是最常踩的坑。如果把 wg.Add(1) 放进 goroutine 里,主 goroutine 可能在子 goroutine 执行前就调用了 wg.Wait(),此时计数器还是 0,wg.Wait() 立刻返回,后续子 goroutine 再调 wg.Add(1) 就会导致负计数 panic:panic: sync: negative WaitGroup counter。
-
wg.Add(1)要紧挨着go语句,且在它之前 - 不能依赖“子 goroutine 启动后一定会先执行
Add”,Go 调度不保证这个顺序 - 批量启动时,
wg.Add(len(tasks))比循环里多次Add(1)更安全(避免循环变量闭包问题)
defer wg.Done() 是惯用但需警惕的写法
defer wg.Done() 看似简洁,但它只在函数 return 时触发。如果 goroutine 中有 panic、提前 return 或逻辑分支没走到 defer 行,Done 就不会被调用,wg.Wait() 永远阻塞。
- 确保
defer wg.Done()在函数入口处就注册,且所在函数一定会执行到 return - 若 goroutine 内部有多个 exit path(比如 error return、break、os.Exit),要检查每个路径是否都覆盖了
Done - 更稳妥的做法是把
wg.Done()放在函数末尾显式调用,而非依赖 defer(尤其在复杂控制流中)
WaitGroup 值不能复制,只能取地址传参
sync.WaitGroup 内部含 noCopy 字段,一旦被复制(比如作为函数参数值传递、赋值给新变量),Go vet 会报错,运行时也可能出现计数错乱 —— 因为两个副本维护各自的计数器,Done 减的是副本的,主 goroutine 等的却是原值。
- 函数参数必须是
*sync.WaitGroup,不是sync.WaitGroup - 切忌写
go f(wg)(值传递),应写go f(&wg) - 结构体字段若需持有 WaitGroup,声明为
wg *sync.WaitGroup,初始化时用&sync.WaitGroup{}
WaitGroup 不处理错误,也不提供超时机制
wg.Wait() 是纯阻塞等待,不关心子 goroutine 是否 panic、是否卡住、是否耗时过长。它只认计数器是否归零。
- 错误收集需配合
chan error或其他同步手段,不能靠 WaitGroup - 需要超时控制时,必须用
select+time.After包裹wg.Wait(),例如:select { case (其中 <code>done是封装了wg.Wait()的 goroutine) - WaitGroup 本身无 context 支持,无法响应 cancel;如需取消,得靠外部信号 + 子 goroutine 主动退出逻辑
真正难的不是记住三个方法名,而是理解计数器变化和 goroutine 执行时机之间的竞态边界 —— 这个边界稍一滑出,程序就既不 panic 也不报错,只是结果不对或永远卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











