并发
go
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// 干活...
fmt.Println(n)
}(i)
}
wg.Wait() // 等全部完成
fmt.Println("全部跑完了")1. sync.WaitGroup 是什么
可以把它想成一个计数器 + 等待门:
| 方法 | 作用 | 前端类比 |
|---|---|---|
wg.Add(1) | 计数 +1,表示「又多了一个任务」 | pendingCount++ |
wg.Done() | 计数 -1,表示「有一个任务完成了」 | pendingCount-- |
wg.Wait() | 阻塞,直到计数变成 0 | await Promise.all(...) |
它不是「记录总共有多少个 goroutine」,而是:
- 你手动告诉它:我要启动几个任务(
Add) - 每个任务完成时手动告诉它:我完成了(
Done) Wait只关心:计数器是不是已经归零
2. defer 是什么
defer = 「这个函数退出时再执行」,不管函数是正常 return 还是 panic。
go
go func(n int) {
defer wg.Done() // 注册:我退出时一定要执行 Done
fmt.Println(n) // 先干活
// 函数结束 → 自动执行 wg.Done()
}(i)前端类比:
javascript
async function work(n) {
try {
console.log(n)
} finally {
pendingCount-- // 无论成功失败,都要减 1
}
}为什么用 defer 而不是直接写 wg.Done()?
- 写在最后也可以,但中间如果有
return、报错,Done可能被跳过 defer保证一定会执行,计数不会漏减
3. Wait 在外面,怎么知道里面跑完了?
关键:WaitGroup 是共享变量,不是每个 goroutine 私有的。
go
var wg sync.WaitGroup // 主 goroutine 创建,所有 goroutine 共用同一个 wg执行顺序大致是这样:
主 goroutine 子 goroutine(10000 个)
──────────── ────────────────────────
wg.Add(1) → 计数 = 1
启动 go func(0)
wg.Add(1) → 计数 = 2
启动 go func(1)
...
wg.Add(1) → 计数 = 10000
启动 go func(9999)
fmt.Println(0)
defer → wg.Done() → 计数 9999
fmt.Println(3)
defer → wg.Done() → 计数 9998
...(谁先完成谁先 Done,顺序不固定)
wg.Wait() ← 卡在这里等
... 最后一个 Done → 计数 = 0
wg.Wait() ← 计数归零,继续往下
fmt.Println("全部跑完了")所以:
Wait不在 goroutine 里面,但它盯着同一个wg的计数- 每个 goroutine 结束时
Done()减 1 - 10000 个都
Done了,计数从 10000 变 0,Wait才放行
不是「Wait 去数函数里有几个 defer」,而是你事先 Add 了几次,就要 Done 几次。
4. 两个容易踩坑的点
坑 1:Add 和 go 的顺序
go
// ✅ 正确:先 Add 再启动
wg.Add(1)
go func() {
defer wg.Done()
}()
// ❌ 危险:Wait 可能在 Add 之前就检查了(竞态)
go func() {
wg.Add(1) // 别写在 goroutine 里面
defer wg.Done()
}()坑 2:Add 的总数必须和 Done 次数一致
Add(1)10000 次 → 必须Done()10000 次- 少
Done:Wait永远卡住 - 多
Done:可能 panic
总结
WaitGroup:共享计数器,Add加任务,Done减任务,Wait等到 0defer:保证 goroutine 退出时一定会Done(),不会漏减Wait在外面也能知道:因为它和每个 goroutine 共用同一个wg,谁完成谁减 1,减到 0 就全部完成