waitgroup 的 add 必须在 goroutine 启动前调用,且不可复制、需传指针;它仅同步完成状态,结果收集需配合 channel 或加锁共享变量。

WaitGroup 必须在 goroutine 启动前 Add
很多人在循环启动 goroutine 时,把 wg.Add(1) 放在 goroutine 内部,导致 Wait() 永远阻塞或 panic。这是因为 Add 和 Done 必须配对,且 Add 要在 Go 语句之前调用,否则可能还没来得及 Add 就执行完了 Done。
正确做法是:
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1) // ← 必须在这里,不是在 goroutine 里
go func(j string) {
defer wg.Done()
process(j)
}(job)
}
wg.Wait()
- 如果漏掉
wg.Add(1),Wait()会立即返回(计数为 0),无法等待任何任务 - 如果
Add在 goroutine 内、且没加锁或同步,可能出现竞态:多个 goroutine 同时修改计数器 - 传参时务必用值拷贝(如
(job)),避免闭包引用循环变量
WaitGroup 不能被复制,必须传指针或全局/字段级使用
sync.WaitGroup 是含 mutex 的结构体,复制后会导致内部状态不一致,运行时报 fatal error: sync: WaitGroup is reused before previous Wait has returned。
常见错误写法:
立即学习“go语言免费学习笔记(深入)”;
func badExample(wg sync.WaitGroup) { // ← 复制了 wg!
wg.Add(1)
go func() { wg.Done() }()
wg.Wait() // panic!
}
- 函数参数、返回值、赋值语句中都禁止直接传递
WaitGroup值类型 - 应传
*sync.WaitGroup,或作为结构体字段、包级变量使用 - 方法接收者也必须是
*WaitGroup,标准库所有方法都是指针接收
WaitGroup 不提供结果收集能力,需配合 channel 或共享变量
WaitGroup 只解决“是否完成”,不解决“结果是什么”。想汇总多个 goroutine 的返回值,不能只靠它。
典型组合方式:
- 用
chan接收每个 goroutine 的输出(推荐,天然并发安全) - 用互斥锁 + 全局 slice/map 收集结果(需注意初始化和锁粒度)
- 避免在 goroutine 中直接向未加锁的公共 slice
append,会引发 panic 或数据丢失
例如安全收集字符串结果:
ch := make(chan string, len(jobs))
for _, j := range jobs {
wg.Add(1)
go func(job string) {
defer wg.Done()
ch <- processResult(job)
}(j)
}
go func() {
wg.Wait()
close(ch)
}()
for res := range ch {
results = append(results, res)
}
WaitGroup 不处理 panic,goroutine 崩溃会导致 Wait 永久阻塞
如果某个 goroutine 执行中 panic,且没 recover,Done() 就不会执行,Wait() 将永远卡住 —— 这是最隐蔽的死锁来源之一。
- 务必在 goroutine 内部用
defer func(){if r:=recover();r!=nil{}}()包裹业务逻辑 - 或者统一用封装函数包装 goroutine 启动,自动 recover 并记录日志
- 开发阶段可启用
GODEBUG=asyncpreemptoff=1辅助定位长时间阻塞点(非必需,但有用)
没有 recover 的 goroutine 看似“完成了”,实则 Done() 被跳过,计数器永远不归零。










