Go 中的waitgroup
· 阅读需 4 分钟
WaitGroup 是 Go 标准库 sync 包中的核心同步原语,用于等待一组 goroutine 全部执行完成,解决主线程/主 goroutine 提前退出、无法等待子 goroutine 执行完毕的问题。
一、核心概念(通俗理解)
可以把 WaitGroup 想象成一个“任务计数器”:
- 启动子 goroutine 前,调用
Add(n)给计数器加n(n是要等待的 goroutine 数量); - 每个子 goroutine 执行完后,调用
Done()给计数器减 1(等价于Add(-1)); - 主 goroutine 调用
Wait(),会阻塞直到计数器归 0,即所有子 goroutine 都执行完毕。
二、基本用法(完整代码示例)
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个任务函数,模拟子goroutine的工作
func worker(id int, wg *sync.WaitGroup) {
// 关键:函数退出前必须调用Done(),确保计数器减1
// 使用defer可以避免遗漏(即使函数提前return也会执行)
defer wg.Done()
fmt.Printf("Worker %d 开始工作\n", id)
// 模拟耗时操作(比如网络请求、计算)
time.Sleep(time.Second * 1)
fmt.Printf("Worker %d 完成工作\n", id)
}
func main() {
// 1. 初始化WaitGroup
var wg sync.WaitGroup
// 2. 启动5个goroutine,所以计数器加5
wg.Add(5)
// 3. 循环启动子goroutine
for i := 1; i <= 5; i++ {
go worker(i, &wg) // 注意:必须传WaitGroup的指针,不能传值(值拷贝会导致计数器独立)
}
// 4. 主goroutine等待所有子goroutine完成
fmt.Println("主goroutine等待所有工作完成...")
wg.Wait()
fmt.Println("所有工作已完成,主goroutine退出")
}
代码解释:
- 初始化:
var wg sync.WaitGroup创建一个 WaitGroup 实例; - Add(5):告诉 WaitGroup 需要等待 5 个 goroutine 完成;
- defer wg.Done():子 goroutine 执行完后自动调用 Done(),确保计数器减 1(避免手动调用遗漏);
- 传指针:
worker函数接收*sync.WaitGroup,因为 WaitGroup 是值类型,传值会拷贝新实例,导致主 goroutine 的计数器无法感知子 goroutine 的 Done(); - wg.Wait():主 goroutine 阻塞,直到计数器归 0。
输出示例(顺序可能因 goroutine 调度略有不同):
主goroutine等待所有工作完成...
Worker 1 开始工作
Worker 3 开始工作
Worker 2 开始工作
Worker 4 开始工作
Worker 5 开始工作
Worker 1 完成工作
Worker 3 完成工作
Worker 2 完成工作
Worker 4 完成工作
Worker 5 完成工作
所有工作已完成,主goroutine退出
三、关键注意事项
- Add 必须在启动 goroutine 前调用:如果先启动 goroutine 再调用 Add,可能导致主 goroutine 的 Wait() 先执行(计数器为 0),直接退出,无法等待 子 goroutine;
- 禁止重复 Wait():同一个 WaitGroup 实例的 Wait() 只能调用一次,多次调用会导致 panic;
- 计数器不能为负:Done() 调用次数超过 Add() 的值,会触发 panic;
- WaitGroup 不可重用:计数器归 0 后,再次调用 Add() 并 Wait() 可能导致不可预期的行为(官方不推荐重用)。
四、典型使用场景
- 批量处理任务(如批量下载文件、批量处理数据库数据);
- 并发请求多个接口,等待所有接口返回后汇总结果;
- 测试并发性能(启动多个 goroutine 模拟高并发)。
总结
WaitGroup核心作用是等待一组 goroutine 执行完毕,通过计数器实现同步;- 核心方法:
Add(n)(加计数器)、Done()(减计数器)、Wait()(阻塞到计数器归 0); - 关键坑点:必须传指针、Add 先于 goroutine 启动、Done 不能漏调、计数器不能为负。
