WaitGroup、Once 与 Cond¶
30 秒版(开场)¶
WaitGroup 等一组 goroutine 结束;Once 保证函数至多执行一次;Cond 等待锁保护的条件成立。Go 1.25+ 新代码可优先用
WaitGroup.Go;兼容旧版本时要在启动 goroutine 之前Add。Once.Do中函数即使 panic,也会被视为已经执行过。
3 分钟版(精讲深度)¶
- 是什么:WG 计数器;Once
sync.Once;Condsync.NewCond(Locker)。 - 为什么:批任务汇合;单例 init;避免忙等(相比 spin)。
- 怎么做:Go 1.25+ 用 WG
Go/Wait,旧版本用Add/Done/Wait;Once 用Do(func());Cond 的Wait必须放在谓词循环中。
10 分钟版(原理 + 图示)¶
flowchart TB
subgraph WaitGroup
A[Add n] --> B[go workers]
B --> C[Done x n]
C --> D[Wait 返回]
end
subgraph Once
O1[Do f] --> O2{已执行?}
O2 -->|否| O3[执行 f 一次]
O2 -->|是| O4[直接返回]
end
WaitGroup 陷阱
- 使用
Add/Done时,让子 goroutine 自己Add(1)可能使Wait提前返回;应先Add再启动 goroutine。 - 当计数器为 0 时,正数
Add必须发生在对应Wait之前;上一轮Wait返回后才能开始下一轮复用。 - Go 1.25 的
WaitGroup.Go(f)会完成“计数 + 启动 + Done”;按文档约束,传入的f不应 panic。 Done次数不匹配 → panic 或永久 Wait。
Once
Do(f)中f如果 panic,当前Once仍被视为已经执行过;后续Do不会重试f。- 一个
Do尚未返回时,其他调用会等待;不要在f内再次调用同一个Once.Do,否则会死锁。 - 初始化可能失败且需要重试时,不要直接用
Once隐藏失败;应显式保存结果,或设计受控重试状态机。
Cond vs channel
- Cond:线程(goroutine)等待 共享内存条件,唤醒精细(Signal 一个)。
- Channel:事件传递、所有权。
Cond 模板
mu.Lock()
for !condition() {
cond.Wait() // 释放 mu,醒来再抢
}
// 使用共享状态
mu.Unlock()
生产场景¶
- 并行 fan-out:
errgroup底层类似 WG + error(扩展库)。 - Once 初始化 DB/配置:冷启动延迟集中在首次。
- Cond:连接池空闲连接通知(也可用 channel);工作窃取自定义队列。
排查与工具¶
-race抓 WG Add/Wait 竞态- goroutine dump:大量卡在
WaitGroup.Wait
架构取舍¶
| 原语 | 适用 |
|---|---|
| WaitGroup | 固定批次并行 |
| errgroup | 需首错取消 |
| Once | 懒加载单例 |
| Cond | 锁保护谓词等待 |
| context | 跨 API 取消 |
深挖问答¶
- WG 能 Add(0) 吗? → 可以但不有意义。
- Once 并发 Do? → 一个执行其余阻塞等待完成。
- Cond Wait 为何用 for 不用 if? → Go 的
Wait只会在Signal/Broadcast后返回,但重新拿到锁时条件可能已被其他 goroutine 改变或消费,所以仍要重新检查谓词。 - Signal vs Broadcast? → 单消费者 vs 全唤醒。
- WG 与 channel done? → channel 可传结果,WG 仅计数。
反模式与事故¶
go func(){ wg.Add(1) }()与go work竞态。- Once 里初始化失败无重试,服务永久坏状态。
- Cond 不用循环,偶发逻辑错。
代码示例¶
Go 1.25+:
var wg sync.WaitGroup
for _, task := range tasks {
task := task
wg.Go(func() {
run(task)
})
}
wg.Wait()
兼容 Go 1.24 及更早版本:
var wg sync.WaitGroup
for _, task := range tasks {
task := task
wg.Add(1)
go func() {
defer wg.Done()
run(task)
}()
}
wg.Wait()
见 basis/goroutine/main.go、basis/sync/main.go。