Golang中sync.WaitGroup 等待组的实现
作者:FfHUCisI
本文介绍了Go语言中sync.WaitGroup的使用方法和注意事项,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
一、WaitGroup 解决什么问题
当我们启动了一组 goroutine 去干活,主 goroutine 需要在所有子 goroutine 完成之后再继续执行。比如:
- 并发抓取 10 个网页,全部抓完再汇总结果
- 批量处理文件,所有 worker 跑完再写入汇总报告
- 启动多个微服务健康检查,全通过才算就绪
WaitGroup 就是为这类"等待一组并发任务完成"的诉求设计的。
二、三个核心方法
WaitGroup 的 API 极为简洁——只有 Add、Done、Wait 三个方法:
var wg sync.WaitGroup
// Add(n): 计数器加 n,表示"我又派了 n 个任务"
wg.Add(3)
for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done() // 等价于 Add(-1),表示"我干完了"
// ... 干活 ...
}(i)
}
wg.Wait() // 阻塞直到计数器归零
Done 内部就是 wg.Add(-1),二者语义完全等价。
三、设计哲学:谁派生,谁 Add
这是最容易踩坑的地方。Add 必须在启动 goroutine 之前调用,否则可能 Wait 先执行,直接放行:
// ❌ 错误:wg.Add 在 go 里面
for i := 0; i < 5; i++ {
go func() {
wg.Add(1) // 严重错误!Wait 可能在这之前就返回了
defer wg.Done()
doWork()
}()
}
wg.Wait()
正确的做法是 Add 和 go 语句紧挨在一起:
// ✅ 正确
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
四、WaitGroup 是值类型,必须传指针
WaitGroup 结构体内部维护了一个计数器,不能用做值拷贝的参数传递——副本中的加减不会影响原值。
// ❌ 值传递:计数器的修改对原始 wg 不可见
func run(wg sync.WaitGroup) {
defer wg.Done()
}
// ✅ 指针传递
func run(wg *sync.WaitGroup) {
defer wg.Done()
}
Go 1.20+ 编译器会给出 copylock 警告,但不如一开始就用对。
五、WaitGroup 不能重用"飞行中"的实例
WaitGroup 的内部状态有三个阶段:0 → Add → Wait。Wait 返回后,WaitGroup 自动回到 0 状态可以重用。但如果在 Wait 还没返回时又调用 Add,这属于并发访问内部状态,是数据竞争。
// ❌ 数据竞争!
go func() {
wg.Add(1) // 与下面的 wg.Wait 并发
defer wg.Done()
doWork()
}()
wg.Wait() // 此时可能还没有 Add 完成
六、练习代码
保存为 waitgroup_demo.go:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 模拟从多个数据源获取数据
type DataFetcher struct {
name string
latency time.Duration // 模拟耗时
}
func (f DataFetcher) Fetch() string {
time.Sleep(f.latency)
return fmt.Sprintf("[%s 数据]", f.name)
}
func main() {
rand.Seed(time.Now().UnixNano())
// 三个数据源,各自耗时不同
sources := []DataFetcher{
{name: "数据库", latency: 200 * time.Millisecond},
{name: "Redis", latency: 50 * time.Millisecond},
{name: "远程API", latency: 300 * time.Millisecond},
}
// 结果收集
results := make([]string, len(sources))
var wg sync.WaitGroup
for i, src := range sources {
wg.Add(1) // 在 go 之前 Add
go func(idx int, fetcher DataFetcher) {
defer wg.Done()
results[idx] = fetcher.Fetch()
}(i, src) // 注意:把循环变量传给闭包
}
fmt.Println("等待所有数据源返回...")
wg.Wait()
fmt.Println("全部就绪!\n")
for i, r := range results {
fmt.Printf("数据源 %d (%s): %s\n", i, sources[i].name, r)
}
// —— 进阶:带超时的 WaitGroup ——
fmt.Println("\n——— 超时模式演示 ———")
var wg2 sync.WaitGroup
wg2.Add(1)
go func() {
defer wg2.Done()
time.Sleep(2 * time.Second) // 模拟很慢的任务
fmt.Println("慢任务完成")
}()
// 用一个 channel 来通知"WaitGroup 已经等完了"
done := make(chan struct{})
go func() {
wg2.Wait()
close(done)
}()
select {
case <-done:
fmt.Println("所有任务在超时前完成")
case <-time.After(500 * time.Millisecond):
fmt.Println("超时!不等了")
}
}
运行结果:
等待所有数据源返回...
全部就绪!数据源 0 (数据库): [数据库 数据]
数据源 1 (Redis): [Redis 数据]
数据源 2 (远程API): [远程API 数据]——— 超时模式演示 ———
超时!不等了
七、WaitGroup vs Channel
| 场景 | WaitGroup | Channel |
|---|---|---|
| 等待所有 goroutine 结束 | ✅ 天然适合 | 需要逐个接收,写起来繁琐 |
| 需要收集返回值 | 需额外变量/锁 | 可以直接从 channel 读取结果 |
| 限制并发数 | 不适合 | 用有缓冲 channel 做信号量 |
| 取消/超时 | 需配合 select(见上文) | 配合 select + ctx.Done() |
八、关键要点
| 要点 | 说明 |
|---|---|
| Add 在前 | 必须在 go 语句之前调用 Add |
| defer Done | 确保 goroutine panic 也能 Done |
| 传指针 | WaitGroup 是值类型,传递需取地址 |
| Wait 可重入 | 多个 goroutine 可以同时 Wait |
| 超时模式 | Wait 本身不超时,配合 channel + select 实现 |
| 不能复制 | 复制 WaitGroup 的值会破坏内部状态 |
到此这篇关于Golang中sync.WaitGroup 等待组的实现的文章就介绍到这了,更多相关Golang sync.WaitGroup 等待组内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
