「Go语言面试题」09 - 什么是Go中的WaitGroup?如何使用它来同步Goroutine?

什么是WaitGroup?
在Go语言中,sync.WaitGroup
是一个并发控制原语,用于等待一组Goroutine完成执行。它提供了一种简单而强大的方式来协调多个并发任务,确保主程序在所有Goroutine完成后再继续执行。
var wg sync.WaitGroup // WaitGroup声明
WaitGroup的核心方法
1. Add(delta int)
// 增加等待的Goroutine数量
wg.Add(1) // 增加1个
wg.Add(3) // 增加3个
2. Done()
// 表示一个Goroutine已完成(相当于Add(-1))
defer wg.Done() // 推荐使用defer确保调用
3. Wait()
// 阻塞当前Goroutine,直到所有任务完成
wg.Wait()
基础使用模式
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 增加计数
go func(id int) {
defer wg.Done() // 任务完成时减少计数
fmt.Printf("Goroutine %d 开始\n", id)
time.Sleep(time.Second)
fmt.Printf("Goroutine %d 结束\n", id)
}(i)
}
fmt.Println("等待所有Goroutine完成...")
wg.Wait() // 阻塞直到所有Done()被调用
fmt.Println("所有任务完成!")
}
WaitGroup的四大核心特性
1. 轻量级同步机制
- 基于原子操作实现
- 无锁设计,高性能
- 零值可用(无需初始化)
2. 阻塞等待机制
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(time.Second)
wg.Done()
}()
start := time.Now()
wg.Wait() // 阻塞约1秒
fmt.Println("等待时间:", time.Since(start))
}
3. 动态任务管理
func main() {
var wg sync.WaitGroup
// 动态添加任务
for i := 0; i < 3; i++ {
wg.Add(1)
go worker(&wg, i)
if i == 1 {
// 中途添加额外任务
wg.Add(1)
go worker(&wg, 10)
}
}
wg.Wait()
}
4. 组合使用模式
func main() {
var wg sync.WaitGroup
ch := make(chan int)
// 生产者
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
// 消费者
wg.Add(1)
go func() {
defer wg.Done()
for num := range ch {
fmt.Println("接收:", num)
}
}()
wg.Wait()
}
五大高级使用场景
1. 批量任务处理
func processImages(images []string) {
var wg sync.WaitGroup
results := make(chan string, len(images))
for _, img := range images {
wg.Add(1)
go func(imagePath string) {
defer wg.Done()
// 模拟图像处理
result := resizeImage(imagePath)
results <- result
}(img)
}
// 等待所有任务完成
go func() {
wg.Wait()
close(results) // 所有任务完成后关闭通道
}()
// 收集结果
for res := range results {
fmt.Println("处理结果:", res)
}
}
2. 服务优雅关闭
func (s *Server) Shutdown() {
var wg sync.WaitGroup
// 关闭监听器
wg.Add(1)
go func() {
defer wg.Done()
s.listener.Close()
}()
// 关闭数据库连接
wg.Add(1)
go func() {
defer wg.Done()
s.db.Close()
}()
// 关闭缓存连接
wg.Add(1)
go func() {
defer wg.Done()
s.cache.Close()
}()
// 等待所有资源关闭
wg.Wait()
fmt.Println("服务优雅关闭完成")
}
3. 并发限制器
func limitedConcurrency(tasks []func(), maxConcurrent int) {
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrent) // 信号量
for _, task := range tasks {
wg.Add(1)
sem <- struct{}{} // 获取信号量
go func(t func()) {
defer func() {
<-sem // 释放信号量
wg.Done()
}()
t() // 执行任务
}(task)
}
wg.Wait()
}
WaitGroup的四大常见陷阱
1. 计数器为负
func main() {
var wg sync.WaitGroup
wg.Add(1)
wg.Done()
wg.Done() // panic: sync: negative WaitGroup counter
}
解决方案:
- 确保Done()调用次数不超过Add()调用
- 使用defer调用Done()
2. 过早调用Wait()
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(time.Second)
wg.Done()
}()
wg.Wait() // 正确等待
wg.Add(1) // 错误:在Wait()后再次使用
// ...
}
解决方案:
- 避免在Wait()后重用WaitGroup
- 需要多次使用时创建新的WaitGroup
3. 忘记调用Done()
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
// 忘记调用wg.Done()
}()
wg.Wait() // 永久阻塞
}
解决方案:
- 使用defer确保Done()被调用
- 结合recover防止panic导致Done()未调用
go func() {
defer func() {
if r := recover(); r != nil {
// 处理panic
}
wg.Done()
}()
// 业务代码...
}()
4. 值传递导致的副本问题
func main() {
var wg sync.WaitGroup
wg.Add(1)
go worker(wg) // 值传递创建副本
wg.Wait() // 不会等待副本
}
func worker(wg sync.WaitGroup) {
defer wg.Done()
// ...
}
解决方案:
- 始终使用指针传递WaitGroup
- 使用闭包捕获WaitGroup引用
// 正确做法1:指针传递
go worker(&wg)
// 正确做法2:闭包捕获
go func() {
defer wg.Done()
// ...
}()
性能优化技巧
1. 批量Add减少调用次数
// 低效:多次调用Add
for i := 0; i < 1000; i++ {
wg.Add(1)
go worker(&wg)
}
// 高效:单次调用Add
wg.Add(1000)
for i := 0; i < 1000; i++ {
go worker(&wg)
}
2. 避免在热点路径使用
// 不推荐:在频繁调用的函数中使用
func processItem(item Item, wg *sync.WaitGroup) {
defer wg.Done()
// ...
}
// 推荐:使用其他同步机制(如channel)
results := make(chan Result)
go processItems(items, results)
3. 组合使用sync.Pool
var wgPool = sync.Pool{
New: func() interface{} { return new(sync.WaitGroup) },
}
func getWaitGroup() *sync.WaitGroup {
return wgPool.Get().(*sync.WaitGroup)
}
func putWaitGroup(wg *sync.WaitGroup) {
// 重置状态
*wg = sync.WaitGroup{}
wgPool.Put(wg)
}
WaitGroup与其他同步原语的对比
特性 | WaitGroup | Channel | sync.Mutex |
---|---|---|---|
主要用途 | 等待任务组完成 | 数据传递/信号 | 临界区保护 |
阻塞机制 | Wait()阻塞 | 发送/接收阻塞 | Lock()阻塞 |
并发安全 | 是 | 是 | 是 |
复用性 | 有限 | 高 | 高 |
性能 | 极高 | 高 | 高 |
适用场景 | 任务同步 | 数据流控制 | 资源共享 |
最佳实践总结
-
指针传递:始终使用指针传递WaitGroup
go worker(&wg)
-
defer Done:使用defer确保Done()被调用
defer wg.Done()
-
提前Add:在启动Goroutine前调用Add()
wg.Add(1) go worker(&wg)
-
避免重用:每次任务组使用新的WaitGroup
func processBatch(batch []Item) { var wg sync.WaitGroup // ... }
-
组合超时:与select结合实现超时控制
select { case <-done: case <-time.After(timeout): }
-
错误处理:结合recover防止panic导致阻塞
defer func() { if r := recover() { /* 处理 */ } wg.Done() }()
结论
Go的WaitGroup是并发编程中不可或缺的工具,它提供了:
- 简洁的任务组同步机制
- 高性能的并发控制能力
- 可靠的任务完成保证
- 灵活的多Goroutine协调方案
掌握WaitGroup的正确使用方法和最佳实践,能够帮助开发者构建出更健壮、更高效的并发程序。无论是简单的任务等待,还是复杂的分布式系统协调,WaitGroup都是Go并发工具箱中的核心组件。
