
WaitGroup 不是用来“等协程结束”的工具,而是用来“等待一组任务完成”的计数器——用错地方,程序就卡死或 panic。
WaitGroup.Add() 必须在 goroutine 启动前调用
很多人把 Add() 放在 goroutine 里,结果 main 协程早早就执行了 Wait(),而计数器还是 0,直接返回,后续 goroutine 还没跑完就结束了。
常见错误现象:Wait() 立刻返回,打印日志不全、文件没写完、HTTP 请求没发出去。
- 必须在
go func() {...}()之前调用Done()对应的Add(1) - 如果启动 N 个 goroutine,
Add(N)要一次性调用,或确保所有Add()都发生在任何Wait()之前 - 不能在 goroutine 内部调用
Add()来“动态扩容”,除非你额外加锁——但那就违背 WaitGroup 的设计初衷了
WaitGroup.Done() 和 defer 的配合容易漏掉
忘记调用 Done() 是最隐蔽的 bug:程序不报错,但永远卡在 Wait(),CPU 占用低,调试时看不出哪条路没走完。
立即学习“go语言免费学习笔记(深入)”;
使用场景:函数体复杂、有多个 return 分支(比如 error early return)、或含 recover 的 panic 处理。
- 推荐写法:
defer wg.Done(),放在 goroutine 函数开头 - 不要写成
defer wg.Add(-1)——Add()不是线程安全的减法操作,且语义不清 - 如果 goroutine 内部还启了子 goroutine,别指望父级
Done()能覆盖它们——每个独立任务都该有自己的同步机制
WaitGroup 不能被复制,传参要用指针
Go 中所有 sync 包类型都不可复制。WaitGroup 是 struct,字段含 mutex 和计数器,值传递会拷贝内部状态,导致行为完全错乱。
常见错误现象:panic: sync: WaitGroup is reused before previous Wait has returned;或 Wait() 永远不返回。
- 函数参数必须是
*sync.WaitGroup,不是sync.WaitGroup - 切片或 map 里存 WaitGroup?不行。只能存指针:
[]*sync.WaitGroup - 初始化后不要赋值给另一个变量(如
w2 := wg),这是隐式复制
WaitGroup 不处理 panic,也不保证执行顺序
它只管“数量对得上”,不管谁先谁后、有没有 panic、甚至有没有真正运行。一个 goroutine panic 了,只要 Done() 被调用,WaitGroup 就认为它“完成了”。
性能影响:几乎为零。底层只是原子计数 + futex 等待,没有调度开销。
- 想捕获 panic?必须在每个 goroutine 里自己
recover() - 需要顺序执行?WaitGroup 不提供,改用 channel 或
sync.Once+ 全局状态 - 需要超时控制?
Wait()本身不支持,得套一层select+time.After()
最难绷的是:WaitGroup 的 zero value 是可用的,所以你可能根本没意识到自己正在用未初始化的实例——直到某次并发量变大,计数器溢出或竞争出现,才在生产环境突然挂掉。










