WaitGroup 必须在 Add 前初始化且不可复制;Add 参数须为编译期确定的正整数,需在 goroutine 启动前调用;Wait 后不可再 Add;Wait 不响应 context 超时,应配合 channel 或使用 errgroup.Group。

WaitGroup 必须在 Add 前初始化,且不能在 Add 后再复制
Go 中 sync.WaitGroup 是值类型,但内部包含指针字段(如 state1)。一旦调用 Add(),其内部状态被激活;若此时将 WaitGroup 变量赋值给另一个变量(即发生复制),新副本会丢失对原状态的引用,导致 Wait() 永远阻塞或 panic。
-
var wg sync.WaitGroup是安全的初始化方式;new(sync.WaitGroup)或&sync.WaitGroup{}也合法,但没必要 - 禁止写
wg2 := wg后在 goroutine 中对wg2调用Done()—— 这不会影响原始wg - 常见错误:在循环中启动 goroutine 时,把
wg作为参数传入闭包,却未传地址 —— 应传&wg
Add 的数值必须在 Go 启动前确定,不能靠 runtime 判断
WaitGroup.Add() 的参数必须是已知正整数(或负数用于抵消),不能在 goroutine 内部动态计算后调用。因为 Add() 和 Done() 需要严格配对,且 Add(0) 是合法的(但无实际作用),而 Add(-1) 在计数为 0 时会 panic。
- 正确做法:遍历任务列表前,先
wg.Add(len(tasks)) - 错误模式:
go func() { wg.Add(1); defer wg.Done(); ... }()—— 这会导致竞争,Add 和 Done 不在同一线程上下文,且可能多次 Add 同一逻辑单元 - 若任务数量动态生成(如 channel 消费),应改用
for range+ 外层Add,或换用errgroup.Group
Wait 之后不能再调用 Add,否则 panic: sync: WaitGroup is reused before previous Wait has returned
WaitGroup 不是可重用对象。一旦调用 Wait() 并返回,该实例即进入“已等待”状态;若再次调用 Add(),运行时会直接 panic。这不是 bug,而是设计约束 —— 它强制你明确生命周期。
- 常见误用:在一个函数里反复
wg.Add(); go f(); wg.Wait(); wg.Add(); ... -
解决方法:每次需要等待新一批任务时,声明新的
var wg sync.WaitGroup - 如果确实需复用(如长周期 worker),应避免
Wait(),改用sync.Cond或 channel 通知机制
和 context.WithTimeout 组合时,Wait 不会自动响应取消
WaitGroup.Wait() 是无条件阻塞,它不感知 context.Context。即使你用 context.WithTimeout 控制整体超时,Wait() 仍会等到所有 Done() 调用完毕才返回 —— 这可能导致超时后仍在等待。
立即学习“go语言免费学习笔记(深入)”;
- 正确组合方式:用
select包裹Wait(),配合done := make(chan struct{})手动通知 - 更推荐方案:用
errgroup.Group(来自golang.org/x/sync/errgroup),它原生支持WithContext - 不要试图在 goroutine 中调用
Wait()然后用time.AfterFunc关闭 —— 无法中断正在阻塞的 Wait
func waitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
return true
case <-time.After(timeout):
return false
}
}
WaitGroup 的核心限制其实就一条:它只负责“计数同步”,不负责“生命周期管理”或“错误传播”。一旦你开始纠结“怎么让它支持取消”“怎么复用”“怎么传进闭包”,大概率说明该换抽象了 —— 比如 errgroup、semaphore 或自定义带状态的协调器。










