一个生活中的比喻 🍜
想象你是一个火锅店老板,今天有 3 桌客人同时点了菜:
- 第 1 桌:点了毛肚
- 第 2 桌:点了鸭肠
- 第 3 桌:点了虾滑
你喊了 3 个服务员同时 去准备这三桌的菜。作为老板,你需要确认所有服务员都上完菜之后,才能在系统里点击"本轮上菜完毕"。
这就是 WaitGroup 的作用------等待一组并发任务全部完成。
WaitGroup 是什么?
sync.WaitGroup 是 Go 标准库提供的一个并发同步工具。它本质上就是一个计数器:
| 方法 | 作用 | 火锅店比喻 |
|---|---|---|
Add(n) |
计数器 +n,告诉它有 n 个任务要做 | 老板说:"有 3 桌菜要上" |
Done() |
计数器 -1,表示一个任务完成了 | 服务员说:"我这桌搞定了" |
Wait() |
阻塞等待,直到计数器归零 | 老板等着,直到所有服务员都回来 |
没有 WaitGroup 会怎样?(翻车现场)
go
package main
import (
"fmt"
"time"
)
func serve(table int, dish string) {
time.Sleep(time.Second) // 模拟上菜需要1秒
fmt.Printf("第 %d 桌的 %s 上好了!\n", table, dish)
}
func main() {
go serve(1, "毛肚")
go serve(2, "鸭肠")
go serve(3, "虾滑")
fmt.Println("老板:本轮上菜完毕!")
// 💀 程序直接结束了,服务员还没上完菜呢!
}
运行结果:
老板:本轮上菜完毕!
菜呢??没了!因为 main 函数一结束,整个程序就退出了,3 个协程还没来得及执行完。
这就好比老板还没等服务员上完菜,就直接关店走人了。😅
用 WaitGroup 拯救世界
go
package main
import (
"fmt"
"sync"
"time"
)
func serve(table int, dish string, wg *sync.WaitGroup) {
defer wg.Done() // 上完菜就报告:我搞定了!(计数器-1)
time.Sleep(time.Second)
fmt.Printf("第 %d 桌的 %s 上好了!\n", table, dish)
}
func main() {
var wg sync.WaitGroup
wg.Add(3) // 老板说:有3桌菜要上
go serve(1, "毛肚", &wg)
go serve(2, "鸭肠", &wg)
go serve(3, "虾滑", &wg)
wg.Wait() // 老板在这里等着,直到3个服务员都回来
fmt.Println("老板:本轮上菜完毕!✅")
}
运行结果:
第 2 桌的 鸭肠 上好了!
第 1 桌的 毛肚 上好了!
第 3 桌的 虾滑 上好了!
老板:本轮上菜完毕!✅
注意:前三行的顺序可能每次不同(因为协程是并发执行的),但"上菜完毕"一定在最后!
核心三步走(记住这个就够了)
scss
第一步:创建 var wg sync.WaitGroup
第二步:登记 wg.Add(任务数量)
第三步:开工 go 干活(&wg) ← 干完后调用 wg.Done()
第四步:等待 wg.Wait() ← 在这阻塞,直到所有任务完成
进阶:循环中使用 WaitGroup
实际开发中,我们通常在循环里启动多个协程:
go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
dishes := []string{"毛肚", "鸭肠", "虾滑", "牛肉", "豆腐"}
for i, dish := range dishes {
wg.Add(1) // 每启动一个协程,计数器+1
go func(table int, food string) {
defer wg.Done()
fmt.Printf("第 %d 桌的 %s 上好了!\n", table+1, food)
}(i, dish)
}
wg.Wait()
fmt.Println("全部上菜完毕!🎉")
}
常见踩坑点 ⚠️
坑1:忘记传指针
go
// ❌ 错误:值传递,函数内的 Done() 对外面的 wg 无效
func serve(wg sync.WaitGroup) {
defer wg.Done()
}
// ✅ 正确:传指针
func serve(wg *sync.WaitGroup) {
defer wg.Done()
}
坑2:Add 和 Go 的顺序不对
go
// ❌ 错误:先启动协程,再 Add ------ 可能 Wait 的时候计数器还是0
go func() {
wg.Add(1)
defer wg.Done()
// 干活...
}()
wg.Wait()
// ✅ 正确:先 Add,再启动协程
wg.Add(1)
go func() {
defer wg.Done()
// 干活...
}()
wg.Wait()
坑3:Add 的数量和 Done 的次数不匹配
go
wg.Add(3)
// 但只启动了 2 个协程调用 Done()
// 结果:wg.Wait() 永远等下去,程序死锁!💀
WaitGroup vs Channel 该用哪个?
| 场景 | 推荐 |
|---|---|
| 只需要等待一组任务完成 | ✅ WaitGroup |
| 需要从协程中获取返回值 | ✅ Channel |
| 需要控制并发数量 | ✅ Channel + WaitGroup |
| 任务之间有依赖关系 | ✅ Channel |
简单记忆:只等不收用 WaitGroup,又等又收用 Channel。
总结
| 概念 | 一句话解释 |
|---|---|
| WaitGroup | 一个计数器,等所有并发任务完成 |
| Add(n) | 告诉它:我要启动 n 个任务 |
| Done() | 告诉它:我这个任务完成了 |
| Wait() | 阻塞在这,直到所有任务完成 |
就像火锅店老板等服务员上菜一样简单!🍲