WaitGroup 是 Go sync 包的轻量同步原语,需手动编排任务协调;Add 必须在 goroutine 启动前调用且不可并发,Done 应用 defer 保证执行。

WaitGroup 本身不是框架组件,而是 Go 标准库 sync 包里的一个轻量同步原语;它不提供调度、生命周期管理或错误聚合能力,所有“协调复杂并行任务”的逻辑必须由你手动编排。直接套用框架思维(比如“注入 WaitGroup”“自动等待”)反而容易引发 panic 或死锁。
WaitGroup.Add 必须在 goroutine 启动前调用,且不能并发调用
常见错误是把 Add(1) 放进 goroutine 内部,或在循环中漏掉、多调、错序调用:
- 如果
wg.Add(1)在go func() { ... }()之后执行,Wait()可能永远阻塞(计数器始终为 0) - 如果多个 goroutine 同时执行
wg.Add(1),而此时已有 goroutine 正在Wait(),Go 运行时会 panic 报错:sync: WaitGroup misuse: Add called concurrently with Wait - 正确姿势:循环体外确定总任务数,或在启动每个 goroutine 前立刻调用
Add(1),且确保无竞态
Done 必须保证执行,且不能重复调用
defer wg.Done() 是最安全的写法,但要注意闭包捕获变量的问题:
错误示例:
立即学习“go语言免费学习笔记(深入)”;
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i) // 所有 goroutine 都打印 3
}()
}
原因:i 是循环变量,被所有匿名函数共享。应显式传参:
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println(id) // 正确输出 0,1,2
}(i)
}
另外,Done() 调用次数超过 Add() 总和会导致 panic:sync: negative WaitGroup counter —— 比如异常路径没走 defer,或误在循环里多次调用 Done()。
WaitGroup 无法处理超时、取消或错误传播
它只回答一个问题:“所有任务是否都调用了 Done()?” 其他需求必须组合其他机制:
- 超时:用
select+time.After()包裹wg.Wait(),否则主线程可能无限挂起 - 取消:需额外传入
context.Context,并在每个 goroutine 中监听ctx.Done() - 错误收集:不能依赖
WaitGroup自身,要配合chan error或切片 + 互斥锁(sync.Mutex)汇总
例如,简单错误收集推荐用带缓冲的 channel(容量 = 任务数),避免 goroutine 因发送阻塞而无法退出:
errCh := make(chan error, len(files))
for _, f := range files {
wg.Add(1)
go func(name string) {
defer wg.Done()
if err := process(name); err != nil {
errCh <- err // 不阻塞
}
}(f)
}
wg.Wait()
close(errCh)
for err := range errCh {
// 处理错误
}
WaitGroup 不可复制,也不宜长期复用
sync.WaitGroup 是含 mutex 和原子计数器的结构体,值拷贝会破坏内部状态,导致 panic 或未定义行为。必须传指针(&wg)或作为局部变量使用。
同时,它不支持重置(no Reset() 方法)。若需多次等待不同批次任务,不要反复 wg = sync.WaitGroup{}(虽语法合法,但旧 goroutine 若仍在运行并调用 Done(),会操作已失效的内存);更稳妥的做法是每次新建一个 var wg sync.WaitGroup。
真正容易被忽略的是:WaitGroup 的零值是可用的,但它的内部计数器和等待队列是惰性初始化的——这意味着你不能在未调用 Add() 前就调用 Wait(),也不能在 Wait() 返回后继续对同一个实例调用 Add(),除非你 100% 确保所有旧 goroutine 已终止且无残留 Done() 调用。


















