waitgroup.add必须在goroutine启动前调用,否则wait可能提前返回;不能复制waitgroup,须传指针;done调用次数必须严格等于add总和;不适用于等待已启动的goroutine。

WaitGroup.Add 必须在 goroutine 启动前调用
很多人把 Add(1) 放在 goroutine 内部,结果 Wait() 立刻返回,后续逻辑出错。本质是:WaitGroup 计数器必须在 goroutine 开始执行前就“预约”好,否则调度器可能在 Add 之前就让主 goroutine 跑完 Wait。
- 错误写法:
go func() { wg.Add(1); defer wg.Done(); ... }()——Add和Done在同一 goroutine 里,但Wait已无计数可等 - 正确顺序:先
wg.Add(1),再go func() { defer wg.Done(); ... }() - 批量启动时,
Add(n)一定要在所有go语句之前,不能放在循环体内部(除非你明确要动态增减)
WaitGroup 不能被复制,必须传指针
Go 中结构体默认值传递,sync.WaitGroup 包含 mutex 和原子变量,复制后两个实例完全独立,原 WaitGroup 的 Add/Done 对副本无效,Wait 永远阻塞或提前返回。
- 常见错误:函数参数写
func process(wg sync.WaitGroup),然后传入wg变量 —— 实际上传的是副本 - 必须写成
func process(wg *sync.WaitGroup),调用时传&wg - 方法接收者也同理:如果定义了
func (w *Worker) Start(wg sync.WaitGroup),就是错的;得是func (w *Worker) Start(wg *sync.WaitGroup)
Done 调用次数必须严格等于 Add 的总和
Done() 是 Add(-1) 的别名,多次调用或漏调都会导致 panic 或死锁。运行时检测到负计数会直接 panic:panic: sync: negative WaitGroup counter。
- 常见错误:recover 捕获 panic 后没处理
Done,或者分支逻辑中某条路径漏掉defer wg.Done() - 推荐写法:始终用
defer wg.Done(),且确保它在 goroutine 最外层作用域(不要包在 if 或子函数里) - 调试技巧:在
Done前加日志,如log.Printf("done, remain=%d", wg.counter)(需反射读取,生产慎用),或用-race检测竞态
WaitGroup 不适用于等待“已启动”的 goroutine
WaitGroup 只能协调“你主动启动”的 goroutine,无法感知外部 goroutine 的生命周期。比如启动一个长期运行的服务协程,之后想用 Wait 等它退出 —— 这不行,因为 Add 已调过,Done 却不知何时触发。
立即学习“go语言免费学习笔记(深入)”;
- 典型误用场景:在
http.ListenAndServe后调wg.Wait(),以为能等服务器关闭 —— 实际上服务器协程不调Done,Wait永远卡住 - 替代方案:用
context.Context控制生命周期,配合 channel 通知退出;或用sync.Once+sync.Cond手动同步状态 - WaitGroup 的定位很窄:只管“我启了多少、它们干完没”,不管“它们在干什么、能不能中断、有没有超时”
最易被忽略的一点:WaitGroup 的 zero value 是可用的,但一旦开始使用(Add/Wait/Done),就不能再赋值或重新声明同名变量 —— Go 不报错,但语义已乱。别把它当普通 struct 随便重置。










