sync.WaitGroup 是等待多个 goroutine 完成的最直接可靠方式,通过原子计数器管理任务数量,需在启动前调用 Add()、结束前用 defer 调用 Done()、主线程调用 Wait() 阻塞等待,避免计数负值、漏计或 panic 导致未完成。

用 sync.WaitGroup 等待多个 goroutine 完成是最直接可靠的方式
Go 标准库的 sync.WaitGroup 就是为这个场景设计的:主线程不阻塞启动 goroutine,又能在所有子任务结束后再继续。它内部通过原子计数器管理 goroutine 数量,比手动 channel 通信或轮询更轻量、不易出错。
关键点在于:Add() 必须在 goroutine 启动前调用(否则可能漏计数),Done() 必须在每个 goroutine 结束前调用(通常用 defer 保底),Wait() 在主线程中阻塞直到计数归零。
常见错误现象包括:
-
panic: sync: negative WaitGroup counter——Done()调用次数多于Add() - 主线程提前退出,部分 goroutine 没执行完 ——
Add()漏调或位置不对 - goroutine 中 panic 导致
Done()未执行 —— 忘记用defer包裹
示例写法:
立即学习“go语言免费学习笔记(深入)”;
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 必须在 goroutine 启动前
go func(id int) {
defer wg.Done() // 确保 panic 时也能计数减一
fmt.Println("done", id)
}(i)
}
wg.Wait() // 主线程在此阻塞
为什么不能只靠 time.Sleep 或空 select{}
这两种方式看似“让主线程等一会儿”,但本质不可靠。
time.Sleep 依赖预估耗时:太短会提前退出,太长拖慢整体流程;且无法感知 goroutine 是否真完成了工作(比如中间有网络重试、IO 阻塞)。
空 select{} 是永久阻塞,根本没提供“完成信号”,只能用于无限等待,完全不适用于需要同步完成的场景。
使用场景上,只有调试临时观察输出时才可能用 time.Sleep,生产代码里必须避免。
当 goroutine 需要返回结果或错误时,sync.WaitGroup 要配合其他机制
sync.WaitGroup 只管“是否结束”,不管“结果是什么”。如果每个 goroutine 要返回值或错误,得额外加结构来收集。
推荐组合方式是:sync.WaitGroup + channel(带缓冲)或 slice + mutex:
- 用 channel 收集结果更符合 Go 的并发风格,尤其适合结果数量不确定或需流式处理
- 用 slice +
sync.Mutex更省内存、无 goroutine 泄漏风险,适合结果数量固定且已知 - 不要在 goroutine 里直接往未加锁的全局 slice 写 —— 数据竞争会导致 panic 或静默错误
性能影响很小:一个 WaitGroup 实例仅占 3 个 word(24 字节),原子操作开销可忽略;但若用无缓冲 channel 做结果收集,可能因接收方未及时读导致 goroutine 阻塞,反而卡住整个等待逻辑。
注意 WaitGroup 的复用限制和生命周期
sync.WaitGroup 不支持复用:一旦 Wait() 返回,内部计数器归零,再次 Add() 会 panic(Go 1.20+ 默认开启检查)。所以每次等待新一批 goroutine,都要用新的 WaitGroup 实例或重新初始化。
容易被忽略的点:
- 把
WaitGroup当参数传进 goroutine 时,务必传指针(*sync.WaitGroup),否则副本间计数不共享 - 不要在
Wait()返回后还对同一个实例调用Add(),除非你显式重置(但标准库没提供 Reset 方法,只能新建) - 在 HTTP handler 或长期运行服务中反复使用时,建议局部声明,避免意外跨请求复用
复杂点往往不在语法,而在计数时机和作用域——写完记得检查每处 Add 和 Done 是否配对、是否在正确作用域内执行。


















