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
  • 批处理/数据处理 → 两者结合使用
相关推荐
bjxiaxueliang1 天前
一文掌握SpringBoot:HTTP服务开发从入门到部署
spring boot·后端·http
饺子大魔王的男人1 天前
Remote JVM Debug+cpolar 让 Java 远程调试超丝滑
java·开发语言·jvm
花酒锄作田1 天前
MCP官方Go SDK尝鲜
golang·mcp
兩尛1 天前
c++知识点2
开发语言·c++
fengfuyao9851 天前
海浪PM谱及波形的Matlab仿真实现
开发语言·matlab
xiaoye-duck1 天前
C++ string 底层原理深度解析 + 模拟实现(下)——面试 / 开发都适用
开发语言·c++·stl
Hx_Ma161 天前
SpringMVC框架提供的转发和重定向
java·开发语言·servlet
期待のcode1 天前
原子操作类LongAdder
java·开发语言
lly2024061 天前
C 语言中的结构体
开发语言
JAVA+C语言1 天前
如何优化 Java 多主机通信的性能?
java·开发语言·php