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

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

重难点

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

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

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

    go 复制代码
    var wg1 sync.WaitGroup
    wg1.Add(1)
    // wg2 := wg1  // 错误!WaitGroup 不应复制
  4. 避免在 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")
    }
}

重难点

  1. 错误传播机制

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

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

    go 复制代码
    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. 简单的并行计算

    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
    }
  2. 并发数据收集

    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
    }
  3. 批处理任务

    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 适用场景

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

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

    go 复制代码
    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. 需要资源清理的初始化

    go 复制代码
    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. 有依赖关系的任务链

    go 复制代码
    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.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
  • 批处理/数据处理 → 两者结合使用
相关推荐
古城小栈2 分钟前
Rust 迭代器产出的引用层数——分水岭
开发语言·rust
ghie909012 分钟前
基于MATLAB的TLBO算法优化实现与改进
开发语言·算法·matlab
恋爱绝缘体112 分钟前
2020重学C++重构你的C++知识体系
java·开发语言·c++·算法·junit
wuk99812 分钟前
VSC优化算法MATLAB实现
开发语言·算法·matlab
AI小怪兽31 分钟前
基于YOLOv13的汽车零件分割系统(Python源码+数据集+Pyside6界面)
开发语言·python·yolo·无人机
Z1Jxxx44 分钟前
加密算法加密算法
开发语言·c++·算法
Eric.Lee20211 小时前
python实现 mp4转gif文件
开发语言·python·手势识别·手势交互·手势建模·xr混合现实
EntyIU1 小时前
python开发中虚拟环境配置
开发语言·python
踏浪无痕1 小时前
AI 时代架构师如何有效成长?
人工智能·后端·架构
程序员小假2 小时前
我们来说一下无锁队列 Disruptor 的原理
java·后端