1. sync.WaitGroup
基本介绍
sync.WaitGroup 是 Go 标准库中用于等待一组 goroutine 完成执行的同步原语。
核心方法
go
type WaitGroup struct {
// 内部字段
}
func (wg *WaitGroup) Add(delta int) // 增加等待计数
func (wg *WaitGroup) Done() // 完成一个任务(计数-1)
func (wg *WaitGroup) Wait() // 阻塞直到计数为0
使用示例
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 为每个 goroutine 增加计数
go func(id int) {
defer wg.Done() // goroutine 结束时减少计数
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All workers completed")
}
重难点
-
Add 必须在 goroutine 启动前调用
go// 正确 wg.Add(1) go func() { defer wg.Done(); /* 工作 */ }() // 错误 go func() { wg.Add(1) // 可能在 Wait 之后执行,导致 Wait 过早返回 defer wg.Done() /* 工作 */ }() -
Done 必须在所有路径上调用
gogo func() { defer wg.Done() // 使用 defer 确保在所有返回路径上都调用 if err := doWork(); err != nil { return // defer 仍会调用 wg.Done() } // 其他处理 }() -
WaitGroup 不能复制
govar wg1 sync.WaitGroup wg1.Add(1) // wg2 := wg1 // 错误!WaitGroup 不应复制 -
避免在 goroutine 中调用 Add
go// 反模式 for i := 0; i < 10; i++ { go func() { wg.Add(1) // 竞态条件! defer wg.Done() // ... }() }
2. errgroup.Group
基本介绍
errgroup.Group 来自 golang.org/x/sync/errgroup 包,在 WaitGroup 基础上增加了:
- 错误传播机制
- 上下文取消功能
- 当有 goroutine 返回错误时,自动取消其他 goroutine
核心方法
go
/*
WithContext(ctx Context) (*Group, Context):可传入自定义 context,实现更灵活的取消控制(如超时、外部信号取消)。
*/
func WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)//启动一个 goroutine 执行函数 f,若 f 返回错误,会记录第一个错误,并通过内置 context 发送取消信号;
func (g *Group) Wait() error//阻塞直到所有 goroutine 完成,返回第一个发生的错误(后续错误会被忽略);
func (g *Group) SetLimit(n int) // 限制并发数
func (g *Group) TryGo(f func() error) bool
使用示例
go
package main
import (
"context"
"fmt"
"time"
"errors"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
// 启动多个并发任务
for i := 1; i <= 3; i++ {
id := i
g.Go(func() error {
fmt.Printf("Worker %d starting\n", id)
// 模拟工作,可能返回错误
select {
case <-time.After(time.Duration(id) * time.Second):
if id == 2 {
return errors.New("worker 2 failed")
}
fmt.Printf("Worker %d done\n", id)
return nil
case <-ctx.Done():
// 其他任务失败,收到取消信号
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
return ctx.Err()
}
})
}
// 等待所有任务完成,返回第一个错误
if err := g.Wait(); err != nil {
fmt.Printf("One of the workers failed: %v\n", err)
} else {
fmt.Println("All workers completed successfully")
}
}
重难点
-
错误传播机制
gog.Go(func() error { if err := doSomething(); err != nil { return fmt.Errorf("task failed: %w", err) // 错误会被传播 } return nil }) -
上下文取消集成
gog, ctx := errgroup.WithContext(context.Background()) g.Go(func() error { // 创建一个可取消的上下文 subCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() return doTaskWithTimeout(subCtx) }) -
并发数限制
gog, _ := errgroup.WithContext(context.Background()) g.SetLimit(3) // 最多同时运行3个goroutine for i := 0; i < 10; i++ { g.Go(func() error { // 最多3个同时执行 return doWork() }) }
3. 使用场景对比
适用场景举例
sync.WaitGroup 适用场景
无错误场景的批量 goroutine 等待(如批量写入日志、无返回值的异步任务、纯消费型 goroutine)
-
简单的并行计算
go// 并行计算斐波那契数列 func parallelFibonacci(n int) int { var wg sync.WaitGroup ch := make(chan int, 2) wg.Add(2) go func() { defer wg.Done(); ch <- fib(n-1) }() go func() { defer wg.Done(); ch <- fib(n-2) }() wg.Wait() close(ch) return <-ch + <-ch } -
并发数据收集
go// 并发获取多个URL func fetchURLs(urls []string) []string { var wg sync.WaitGroup results := make([]string, len(urls)) for i, url := range urls { wg.Add(1) go func(idx int, u string) { defer wg.Done() resp, _ := http.Get(u) results[idx] = resp.Status }(i, url) } wg.Wait() return results } -
批处理任务
go// 批量处理文件 func processFiles(files []string) { var wg sync.WaitGroup for _, file := range files { wg.Add(1) go func(f string) { defer wg.Done() processFile(f) // 忽略错误 }(file) } wg.Wait() }
errgroup.Group 适用场景
需要错误感知 + 快速失败的并发场景(如批量接口调用、多数据源查询、分布式任务调度,一个失败则终止所有)
-
需要错误处理的微服务调用,任一服务失败,整个操作失败
gofunc callServices(ctx context.Context) error { g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return callUserService(ctx) }) g.Go(func() error { return callOrderService(ctx) }) g.Go(func() error { return callPaymentService(ctx) }) return g.Wait() } -
需要资源清理的初始化
gofunc initializeComponents() error { g, ctx := errgroup.WithContext(context.Background()) // 并行初始化多个组件 var db *sql.DB g.Go(func() error { var err error db, err = initDatabase(ctx) return err }) var cache *redis.Client g.Go(func() error { var err error cache, err = initCache(ctx) return err }) // 如果有初始化失败,确保清理 if err := g.Wait(); err != nil { if db != nil { db.Close() } if cache != nil { cache.Close() } return err } return nil } -
有依赖关系的任务链
gofunc pipelineProcessing(data []string) error { g, ctx := errgroup.WithContext(context.Background()) // 第一阶段:数据预处理 stage1 := make(chan string, len(data)) g.Go(func() error { defer close(stage1) for _, item := range data { processed, err := preprocess(ctx, item) if err != nil { return err } select { case stage1 <- processed: case <-ctx.Done(): return ctx.Err() } } return nil }) // 第二阶段:数据转换 stage2 := make(chan Result, len(data)) g.Go(func() error { defer close(stage2) for item := range stage1 { result, err := transform(ctx, item) if err != nil { return err } select { case stage2 <- result: case <-ctx.Done(): return ctx.Err() } } return nil }) return g.Wait() }
4. 核心区别总结
| 特性 | sync.WaitGroup | errgroup.Group |
|---|---|---|
| 错误处理 | 无内置支持,需手动处理 | 自动传播第一个错误,支持错误取消 |
| 上下文集成 | 无 | 支持 context 传播和取消 |
| 并发控制 | 无 | 支持 SetLimit 限制并发数 |
| 易用性 | 较简单 | 功能丰富,但稍复杂 |
| 内存使用 | 较小 | 稍大(维护额外状态) |
| 标准库 | 是 | 需要额外导入 |
| 适用场景 | 简单并行任务 | 需要错误处理的复杂任务链 |
5. 高级使用技巧
组合使用模式
go
func complexOperation() error {
var wg sync.WaitGroup
errCh := make(chan error, 1)
// 使用 WaitGroup 并行执行独立任务
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if err := independentTask(id); err != nil {
select {
case errCh <- fmt.Errorf("task %d failed: %w", id, err):
default:
}
}
}(i)
}
// 使用 errgroup 处理有依赖的任务链
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
return dependentTaskChain(ctx)
})
// 等待所有任务完成
wg.Wait()
close(errCh)
// 检查独立任务的错误
if err := <-errCh; err != nil {
return err
}
// 检查依赖任务的错误
return g.Wait()
}
带超时控制的 errgroup
go
func operationWithTimeout(timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < 3; i++ {
g.Go(func() error {
return doTaskWithContext(ctx)
})
}
return g.Wait()
}
6. 常见陷阱与解决方案
WaitGroup 陷阱
go
// 陷阱1:计数不匹配
func trap1() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
// 忘记调用 wg.Done()
}()
wg.Wait() // 永远阻塞
}
// 解决方案:始终使用 defer
func solution1() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() // 确保调用
// 任务逻辑
}()
wg.Wait()
}
errgroup 陷阱
go
// 陷阱:错误处理中的资源泄漏
func trap2() error {
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
conn, err := acquireConnection()
if err != nil { return err }
// 如果后续有错误,conn 可能泄漏
return doWork(ctx, conn)
})
return g.Wait()
}
// 解决方案:使用 defer 清理
func solution2() error {
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
conn, err := acquireConnection()
if err != nil { return err }
defer conn.Close() // 确保清理
return doWork(ctx, conn)
})
return g.Wait()
}
总结
sync.WaitGroup 是最基础的并发同步原语,适用于简单并行场景,需要手动处理错误和资源管理。
errgroup.Group 是更高级的抽象,适合复杂场景,提供:
- 自动错误传播
- 上下文集成
- 并发控制
- 任务协调
选择建议:
- 任务独立且简单 → sync.WaitGroup
- 需要错误处理/任务协调 → errgroup.Group
- 高并发简单任务 → sync.WaitGroup
- 微服务/分布式系统 → errgroup.Group
- 批处理/数据处理 → 两者结合使用