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 小时前
Go语言编码规范
开发语言·后端·golang
无限大.2 小时前
为什么“云计算“能改变世界?——从本地计算到云端服务
开发语言·云计算·perl
码luffyliu2 小时前
Go 实战: “接口 + 结构体” 模式
后端·go
Lisonseekpan2 小时前
为什么Spring 推荐使用构造器注入而非@Autowired字段注入?
java·后端·spring·log4j
BingoGo2 小时前
PHP 之高级面向对象编程 深入理解设计模式、原则与性能优化
后端·php
草莓熊Lotso2 小时前
Python 流程控制完全指南:条件语句 + 循环语句 + 实战案例(零基础入门)
android·开发语言·人工智能·经验分享·笔记·后端·python
laozhoy12 小时前
深入理解Golang中的锁机制
开发语言·后端·golang
码luffyliu2 小时前
Go 中的深浅拷贝:从城市缓存场景讲透指针与内存操作
后端·go·指针·浅拷贝·深拷贝
雾岛听蓝2 小时前
C++ 模板初阶
开发语言·c++