Go 语言并发编程:sync.WaitGroup 实战指南
前言
在 Go 语言的并发编程中,`sync.WaitGroup` 是最常用的同步原语之一。它用于等待一组 goroutine 完成执行,是协调并发任务的核心工具。本文将通过实际代码示例,深入讲解 WaitGroup 的使用方法和最佳实践。
什么是 sync.WaitGroup?
`sync.WaitGroup` 是一个计数器,用于等待一组 goroutine 完成。它提供了三个核心方法:
-
`Add(delta int)`:增加计数器
-
`Done()`:减少计数器(相当于 Add(-1))
-
`Wait()`:阻塞直到计数器归零
基础用法示例
```go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers completed!")
}
```
**输出:**
```
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 1 done
Worker 2 done
Worker 3 done
All workers completed!
```
实战场景:并发抓取网页
```go
package main
import (
"fmt"
"io"
"net/http"
"sync"
)
func fetchURL(url string, wg *sync.WaitGroup, results chan<- string) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
results <- fmt.Sprintf("%s: Error - %v", url, err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
results <- fmt.Sprintf("%s: %d bytes", url, len(body))
}
func main() {
urls := []string{
}
var wg sync.WaitGroup
results := make(chan string, len(urls))
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg, results)
}
// 等待所有 goroutine 完成
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Println(result)
}
}
```
常见陷阱与注意事项
1. Add 必须在 goroutine 之前调用
```go
// ❌ 错误写法
for i := 0; i < 3; i++ {
go func() {
wg.Add(1)
// work...
wg.Done()
}()
}
// ✅ 正确写法
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// work...
}()
}
```
2. 使用 defer 确保 Done 被调用
```go
func process(id int, wg *sync.WaitGroup) {
defer wg.Done() // 确保即使 panic 也会执行
// 处理逻辑...
}
```
3. 避免 WaitGroup 值传递
```go
// ❌ 错误:值传递会导致复制
func worker(wg sync.WaitGroup) {
wg.Done()
}
// ✅ 正确:指针传递
func worker(wg *sync.WaitGroup) {
wg.Done()
}
```
高级用法:结合 context 实现超时控制
```go
package main
import (
"context"
"fmt"
"sync"
"time"
)
func workerWithTimeout(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-time.After(2 * time.Second):
fmt.Printf("Worker %d completed\n", id)
case <-ctx.Done():
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go workerWithTimeout(ctx, i, &wg)
}
wg.Wait()
fmt.Println("All workers finished or cancelled")
}
```
性能对比:WaitGroup vs Channel
| 特性 | WaitGroup | Channel |
|------|-----------|---------|
| 适用场景 | 等待一组任务完成 | 数据传递、流式处理 |
| 代码复杂度 | 低 | 中等 |
| 灵活性 | 较低 | 高 |
| 内存开销 | 小 | 中等 |
总结
`sync.WaitGroup` 是 Go 并发编程的基石之一:
-
**简单易用**:三个方法搞定同步
-
**高效安全**:内部使用原子操作
-
**适用广泛**:从简单任务到复杂并发场景
**最佳实践:**
-
始终在启动 goroutine 之前调用 Add
-
使用 defer 确保 Done 被调用
-
传递指针而非值
-
结合 context 实现超时和取消
掌握 WaitGroup,让你的 Go 并发代码更加优雅可靠!
*如果你觉得这篇文章有帮助,欢迎点赞、收藏、关注!*