golang-- sync.WaitGroup 和 errgroup.Group 详解

1. sync.WaitGroup

基本介绍

sync.WaitGroup 是 Go 标准库中用于等待一组 goroutine 完成执行的同步原语。

核心方法

type WaitGroup struct {
    // 内部字段
}

func (wg *WaitGroup) Add(delta int)  // 增加等待计数
func (wg *WaitGroup) Done()          // 完成一个任务(计数-1)
func (wg *WaitGroup) Wait()          // 阻塞直到计数为0

使用示例

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")
}

重难点

  1. Add 必须在 goroutine 启动前调用

    // 正确
    wg.Add(1)
    go func() { defer wg.Done(); /* 工作 */ }()
    
    // 错误
    go func() {
        wg.Add(1)  // 可能在 Wait 之后执行,导致 Wait 过早返回
        defer wg.Done()
        /* 工作 */
    }()
    
  2. Done 必须在所有路径上调用

    go func() {
        defer wg.Done()  // 使用 defer 确保在所有返回路径上都调用
        
        if err := doWork(); err != nil {
            return  // defer 仍会调用 wg.Done()
        }
        // 其他处理
    }()
    
  3. WaitGroup 不能复制

    var wg1 sync.WaitGroup
    wg1.Add(1)
    // wg2 := wg1  // 错误!WaitGroup 不应复制
    
  4. 避免在 goroutine 中调用 Add

    // 反模式
    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

核心方法

/*
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

使用示例

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")
    }
}

重难点

  1. 错误传播机制

    g.Go(func() error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("task failed: %w", err)  // 错误会被传播
        }
        return nil
    })
    
  2. 上下文取消集成

    g, ctx := errgroup.WithContext(context.Background())
    
    g.Go(func() error {
        // 创建一个可取消的上下文
        subCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
        defer cancel()
        
        return doTaskWithTimeout(subCtx)
    })
    
  3. 并发数限制

    g, _ := 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)

  1. 简单的并行计算

    // 并行计算斐波那契数列
    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
    }
    
  2. 并发数据收集

    // 并发获取多个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
    }
    
  3. 批处理任务

    // 批量处理文件
    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 适用场景

需要错误感知 + 快速失败的并发场景(如批量接口调用、多数据源查询、分布式任务调度,一个失败则终止所有)

  1. 需要错误处理的微服务调用,任一服务失败,整个操作失败

    func 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() 
    }
    
  2. 需要资源清理的初始化

    func 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
    }
    
  3. 有依赖关系的任务链

    func 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.WaitGrouperrgroup.Group
错误处理无内置支持,需手动处理自动传播第一个错误,支持错误取消
上下文集成支持 context 传播和取消
并发控制支持 SetLimit 限制并发数
易用性较简单功能丰富,但稍复杂
内存使用较小稍大(维护额外状态)
标准库需要额外导入
适用场景简单并行任务需要错误处理的复杂任务链

5. 高级使用技巧

组合使用模式

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

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 陷阱

// 陷阱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 陷阱

// 陷阱:错误处理中的资源泄漏
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
  • 批处理/数据处理 → 两者结合使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值