WaitGroup 必须在 goroutine 启动前调用 Add,且 Add 与 Done 需严格配对;不可复用或重置,错误处理需配合 channel 或 errgroup。

WaitGroup 必须在 goroutine 启动前 Add,不能在 goroutine 内部调用 Add
常见错误是把 wg.Add(1) 放在 go func() { ... }() 里,导致 Add 和 Done 不配对,甚至 panic:「panic: sync: negative WaitGroup counter」。这是因为 go 启动是异步的,wg.Wait() 可能早已执行完毕,而 goroutine 还没来得及 Add。
正确做法是:先确定要启动多少个 goroutine,立刻在主 goroutine 中调用 wg.Add(n),再启动它们。
- 若数量动态(比如遍历切片),用
len(tasks)预先 Add - 不要用
defer wg.Done()包裹整个 goroutine 函数体——它只在函数返回时触发,但函数可能永不返回(如死循环) - 每个 goroutine 必须且仅调用一次
wg.Done(),通常放在函数末尾或明确退出路径上
WaitGroup 不能跨 goroutine 复用,也不支持重置
sync.WaitGroup 是一次性结构:一旦 Wait() 返回,内部计数器归零,再次调用 Wait() 会立即返回(即使后续又调用了 Add()),除非你重新声明一个新实例。这不是 bug,是设计使然。
典型误用场景:在一个 for 循环中反复复用同一个 wg 变量,期望它“清空重来”。
立即学习“go语言免费学习笔记(深入)”;
- 需要多次等待不同批次任务?每次循环都声明新的
wg := &sync.WaitGroup{} - 如果逻辑复杂、需多次协调,考虑封装成函数,让
wg在函数作用域内生命周期清晰 - 别试图用反射或 unsafe 重置
WaitGroup——它没有公开的 Reset 方法,强行操作不可靠
WaitGroup 本身不传递错误,错误处理必须显式收集
WaitGroup 只管“是否全部完成”,不管“完成得对不对”。如果你的 goroutine 执行出错(比如 HTTP 请求失败、文件读取异常),wg.Wait() 依然会返回,你根本不知道哪几个失败了。
常见补救方式是配合其他同步原语一起用:
- 用
errChan := make(chan error, n)收集错误,每个 goroutine 执行完写入errChan,主 goroutine 从 channel 读取并判断 - 用
sync.Once+ 全局 error 变量保存第一个错误(适合“任一失败即终止”场景) - 更推荐:改用
errgroup.Group(来自golang.org/x/sync/errgroup),它内置了 WaitGroup + 错误传播,且支持上下文取消
WaitGroup 的零值可用,但别依赖指针传参带来的“隐式共享”
sync.WaitGroup 的零值是有效的,可以直接声明 var wg sync.WaitGroup。但很多人习惯传指针(&wg)进函数,以为“更安全”,其实容易埋坑。
问题在于:如果函数内部做了 wg = sync.WaitGroup{} 赋值(比如想“重置”),由于传的是指针,这个赋值只是改了局部变量,对外部无影响;更糟的是,如果传的是值(wg 而非 &wg),那函数里调用的 Add/Done 根本不会作用到原始实例上——因为复制了一份。
- 始终传指针:
func doWork(wg *sync.WaitGroup),并在函数内只调用方法,不重新赋值结构体 - 避免在函数内重新初始化
wg;如需隔离,让调用方负责新建 - 静态检查工具(如
staticcheck)会警告 “passing a sync.WaitGroup by value”,留意这类提示
WaitGroup 看似简单,但 Add/Wait/Done 的时序、生命周期和错误协作稍有偏差,就会出现静默失败或 panic。最稳妥的做法,是把它当作“纯计数信号量”来用,其余逻辑(错误、取消、超时)交给 channel、context 或 errgroup 分担。


















