waitgroup 必须在启动 goroutine 前调用 add:因 add 与 go 非原子操作,若在 goroutine 内 add 易致 main 提前退出;应先 wg.add(n),再启协程;动态数量则循环 add(1) 在 go 外;add 负数会 panic;defer wg.done() 必须置于 goroutine 入口首行以确保执行;waitgroup 不可复用,每次需新实例;复杂场景优先用 errgroup.group。

WaitGroup 必须在启动 goroutine 前 Add
很多人写 sync.WaitGroup 时,在 go 语句里才调用 wg.Add(1),结果程序提前退出——因为 Add 和 Go 不是原子操作,main 可能已执行完 wg.Wait(),而协程还没来得及注册自己。
正确做法是:先 wg.Add(n),再启动对应数量的 goroutine。
- 如果协程数动态变化(比如从 channel 拉取任务),用循环 +
wg.Add(1)放在go外面,别放 inside -
wg.Add()传负数会 panic,别手抖写成wg.Add(-1) - 不要在 goroutine 里反复
wg.Add(1)试图“续命”,那是设计缺陷,该用 worker pool 就用
defer wg.Done() 要紧贴 goroutine 函数体开头
defer 看似安全,但若放在 goroutine 内部深层逻辑里(比如嵌套函数、条件分支后),一旦 panic 或提前 return,Done() 就漏调了,wg.Wait() 死等。
最稳写法:goroutine 入口第一行就是 defer wg.Done(),哪怕后面立刻 return 也确保触发。
立即学习“go语言免费学习笔记(深入)”;
- 别写
go func() { if err != nil { return }; defer wg.Done(); ... }()——return会跳过defer - 如果 goroutine 里有 recover,
defer wg.Done()仍要放在最外层,recover 不影响它执行 - 不用 defer?也可以手动调
wg.Done(),但必须保证每条退出路径都覆盖到,容易漏
WaitGroup 不能复用,也不能跨生命周期传递
sync.WaitGroup 不是线程安全的“可重置对象”。一旦 wg.Wait() 返回,内部计数器归零,此时再调 wg.Add() 是未定义行为(Go 1.21+ 会 panic)。
常见翻车场景:把同一个 wg 实例传给多个不相关的 goroutine 组,或在一个函数里反复 Wait + Add。
- 每次等待一组新协程,就用新的
sync.WaitGroup{}实例 - 别把
wg当字段塞进结构体长期持有,除非你能严格控制它的生命周期(比如 struct 初始化时 new,销毁时确保 Wait 已完成) - 如果需要“多次等待”,改用
sync.Once+ channel 或errgroup.Group更合适
用 errgroup.Group 替代裸 WaitGroup 的时机
当协程可能返回错误、需要统一取消、或想避免手动管理 WaitGroup 时,errgroup.Group 是更健壮的选择——它内置 cancel、错误传播、且 Go 方法自动处理 Add/Done。
但它不是银弹:引入额外依赖(golang.org/x/sync/errgroup),且默认不支持“等待但忽略错误”这种简单场景。
- 需要任意一个 goroutine 出错就立即停止其余任务?用
eg.Go()+eg.Wait() - 要等全部完成,只关心是否有错?还是用
WaitGroup更轻量 - 和 context 配合取消时,
errgroup.WithContext()自动注入ctx,比手动传参干净得多
真正难的不是写对 WaitGroup,而是判断「这组协程是否真该被 wait」——比如 HTTP handler 启动的后台 goroutine,不该阻塞 server shutdown;这种地方用 context 控制生命周期,比死等更关键。










