waitgroup 的 add 必须在 goroutine 启动前调用,否则 main 协程可能提前退出导致子协程未执行;固定数量时 wg.add(n) 应置于循环外,再启动协程。

WaitGroup 必须在 goroutine 启动前调用 Add
程序提前退出、fmt.Println 没输出、子协程“消失”——十有八九是 wg.Add(1) 写在了 go 语句内部或之后。因为 Add 和 go 不是原子操作,main 协程可能已执行完 wg.Wait(),而协程还没来得及注册自己。
正确顺序只有一种:先确定数量,再启动协程。
- 固定数量(如启动 5 个 worker):
wg.Add(5)放在循环外,然后for i := 0; i - 动态数量(如从 channel 拉任务):
for task := range tasks { wg.Add(1); go process(task, &wg) }——Add(1)必须在go前,且不能放进 goroutine 里 -
Add传负数会 panic,别手抖写成wg.Add(-1)
defer wg.Done() 必须放在 goroutine 函数体第一行
很多人把 defer wg.Done() 写在 if 分支里、error 处理后、甚至 recover 里面,结果一 return 或 panic 就漏调 —— wg.Wait() 会永远卡住。
最稳写法就是:goroutine 入口第一行就是 defer wg.Done(),后面不管怎么分支、提前 return、还是 panic,它都保证执行。
- ✅ 正确:
go func() { defer wg.Done() if err != nil { return // defer 仍会执行 } // ... }() - ❌ 错误:
go func() { if err != nil { return // wg.Done() 根本没注册 } defer wg.Done() }()
WaitGroup 不可复用,别跨生命周期传递
sync.WaitGroup 不是“重置型对象”。一旦 wg.Wait() 返回,计数器归零;此时再调 wg.Add(1) 是未定义行为 —— Go 1.21+ 直接 panic。
这意味着:
- 每次等待新一批协程,必须用新
sync.WaitGroup{}实例,别试图“清零重用” - 不要把同一个
wg实例传给多个不相关的函数或 goroutine 组,容易出现 Add/Wait 时序错乱 - 复杂场景(需错误传播、取消、超时)优先用
errgroup.Group,它内置复用安全和 context 支持
WaitGroup vs Channel:该选哪个?
不是所有“等完成”的场景都适合 sync.WaitGroup。它只回答一个问题:“它们做完没?”,不关心结果、不支持超时、无法取消。
当你需要:
- 仅同步、无数据传递、高吞吐批量任务 → 用
sync.WaitGroup,轻量高效 - 要拿返回值(比如 HTTP 响应、计算结果)→ 用带缓冲 channel 收集
results := make(chan Result, n) - 要控制超时、取消、或 select 多路等待 → 用 channel 配合
context.WithTimeout或time.After
最容易被忽略的一点:WaitGroup 的计数器是“纯信号”,它不感知协程是否 panic、是否卡死、是否逻辑出错 —— 它只认 Done() 调用次数。所以务必确保每个路径都走到 defer wg.Done(),否则就是静默死锁。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











