waitgroup 的 add 必须在启动 goroutine 前调用且值大于 0,否则可能因 done 先于 add 执行导致负计数 panic;defer wg.done() 在 panic 时失效,应配合 recover 或显式调用;可重用但需确保上一轮 wait 已完成;与 context.withtimeout 无冲突,常组合实现超时等待。

WaitGroup 什么时候该 Add,又为什么不能在 goroutine 里 Add
WaitGroup 的 Add 必须在启动 goroutine 之前调用,且传入值必须大于 0。如果在 goroutine 内部调用 Add(1),极大概率触发 panic:panic: sync: negative WaitGroup counter 或死锁——因为 Done() 可能早于 Add() 执行。
根本原因是 Add 和 Done 操作非原子配对:Go 运行时不会帮你校验调用顺序,它只按计数器是否归零来决定 Wait() 返回。一旦计数器被减到负数,就直接 panic。
- 正确做法:先确定任务总数,
wg.Add(n)在 goroutine 启动前一次性完成 - 错误写法:
go func() { wg.Add(1); defer wg.Done(); ... }() - 若任务数动态生成(如遍历 channel),需在外层循环中预估或分批
Add
WaitGroup 配合 defer wg.Done() 为什么有时不生效
defer wg.Done() 看似稳妥,但只在函数返回时触发。如果 goroutine 中发生 panic 且未恢复,defer 不会执行,导致计数器永远不归零,Wait() 无限阻塞。
典型场景是 HTTP handler、JSON 解析或数据库查询出错后直接 panic,而没包一层 recover。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 安全写法:用
defer+recover,或把wg.Done()放在最外层defer后面(但需确保它一定被执行) - 更推荐显式调用:
go func() { defer func() { wg.Done() }(); ... }() - 注意:不要在匿名函数参数里提前调用
wg.Done(),比如go f(wg.Done)—— 这会立即执行
WaitGroup 能否重复使用?重用时要注意什么
WaitGroup 可以重用,但必须确保上一轮 Wait() 已返回,且所有 Done() 已完成。否则新调用 Add() 可能和残留的 Done() 竞态,引发负计数 panic。
官方文档明确说:“The zero value for a WaitGroup is usable”,但没说“任意时刻都可重用”。实际中,重用常出现在循环任务中,比如定时拉取数据。
- 安全重用前提:上一轮
Wait()返回后,无任何 goroutine 还在运行或即将调用Done() - 避免在 WaitGroup 尚未 Wait 完就 Add 新任务(常见于忘记
go启动延迟) - 调试技巧:加日志,或用
runtime.NumGoroutine()辅助判断是否真已结束
WaitGroup 和 context.WithTimeout 一起用会不会冲突
不会冲突,但职责完全不同:WaitGroup 管“是否做完”,context.WithTimeout 管“最多做多久”。两者常组合使用,用来实现带超时的等待。
关键点在于:不能只靠 ctx.Done() 关闭逻辑,还要保证 wg.Wait() 不被卡住。常见错误是 select 里漏了 case 分支,或在 goroutine 中忽略 ctx 是否已取消。
- 推荐结构:用
select包裹Wait(),一个 case 等wg,一个 case 等ctx.Done() - goroutine 内部也要检查
ctx.Err(),及时退出,避免Done()永远不调用 - 别用
time.AfterFunc替代 context —— 它无法主动取消,也不传递取消信号给下游
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










