go在for循环中使用errgroup和channel进行并发处理

errgroup

使用 errgroup 来管理协程,代码示例:

go 复制代码
g, ctx := errgroup.WithContext(ctx)
for _, materialId := range materialIds {
    mid := materialId // 创建副本避免闭包问题
    g.Go(func() error {
        // 检查 workflow 状态
        curWorkflow, _ := db.W().GetWorkflow(ctx, workflow.ID)
        //这里判断是否要继续
        if curWorkflow.Status == enum.WorkflowStatus_Canceled {
            linfo.L(ctx, "[ProcessMaterialDownloading] workflow canceled", "appVerID", appVer)
            return nil
        }

        matRes, matMaterial, mErr := ProcessMaterialDownloading(ctx, mid, workflow, appVer, matProc)
        if mErr != nil {
            return mErr
        }

        // 使用互斥锁保护共享资源
        mu.Lock()
        resToMatRefId[matRes.ID] = matMaterial
        resourceBeDownload = append(resourceBeDownload, *matRes)
        mu.Unlock()

        linfo.L(ctx, "[ProcessMaterialDownloading] process material success", 
            "appVerID", appVer, "materialId", mid)
        return nil
    })
}

// 等待所有协程完成
if err := g.Wait(); err != nil {
    lerror.L(ctx, "[ProcessMaterialDownloading] process material failed", "appVerID", appVer, "err", err)
    return err
}

在这里使用互斥锁是安全的。互斥锁的作用就是确保同一时间只有一个协程可以访问被保护的代码块。当两个协程都走到这个位置时:

第一个协程会获得锁并执行代码块

第二个协程会被阻塞,等待第一个协程释放锁

第一个协程执行完后释放锁

第二个协程获得锁并执行代码块

errgroup 在 Go 中类似于 Java 中的 CompletableFuture 或 ExecutorService 配合 Future 的使用方式。来对比一下Java:

java 复制代码
// 使用 CompletableFuture
List<CompletableFuture<Result>> futures = new ArrayList<>();
for (Long materialId : materialIds) {
    CompletableFuture<Result> future = CompletableFuture.supplyAsync(() -> {
        return processMaterialDownloading(materialId);
    });
    futures.add(future);
}

// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

errgroup主要特点:

go 复制代码
// 1. 与 context 集成
g, ctx := errgroup.WithContext(context.Background())

// 2. 错误传播
g.Go(func() error {
    if err := someTask(); err != nil {
        return err // 第一个错误会取消其他 goroutine
    }
    return nil
})

// 3. 并发限制
g.SetLimit(10) // 限制最大并发数

// 4. 等待所有 goroutine 完成
if err := g.Wait(); err != nil {
    // 处理错误
}

当任何一个协程返回错误时:

errgroup 会取消关联的 context

其他协程可以通过检查 ctx.Err() 来感知取消

但其他协程不会自动停止,需要自己检查 context,比如:

go 复制代码
 g, ctx := errgroup.WithContext(ctx)
    
    for _, id := range materialIds {
        mid := id
        g.Go(func() error {
            // 定期检查 context 是否已取消
            if ctx.Err() != nil {
                return ctx.Err()
            }
            
            // 执行任务
            result, err := processMaterial(ctx, mid)
            if err != nil {
                return err // 这个错误会触发其他协程的取消
            }
            
            // 继续处理...
            return nil
        })
    }
    
    return g.Wait()

channel

或者使用channel来保存处理结果:

go 复制代码
// 创建一个错误通道来收集错误
errChan := make(chan error, len(materialIds))
// 创建一个结果通道来收集处理结果
resultChan := make(chan struct {
    res      *model.Resource
    mat      *model.Material
    materialId int64
}, len(materialIds))

// 启动协程处理每个素材
for _, materialId := range materialIds {
    go func(mid int64) {
        matRes, matMaterial, mErr := ProcessMaterialDownloading(ctx, mid, workflow, appVer, matProc)
        if mErr != nil {
            errChan <- mErr
            return
        }
        resultChan <- struct {
            res      *model.Resource
            mat      *model.Material
            materialId int64
        }{matRes, matMaterial, mid}
    }(materialId)
}

// 等待所有协程完成或出错
for i := 0; i < len(materialIds); i++ {
    select {
    case mErr := <-errChan:
        lerror.L(ctx, "[ProcessMaterialDownloading] process material failed", 
            "appVerID", appVer, "err", mErr)
        return mErr
    case result := <-resultChan:
        // 检查 workflow 状态
        curWorkflow, _ := db.W().GetWorkflow(ctx, workflow.ID)
        if curWorkflow.Status == enum.WorkflowStatus_Canceled {
            linfo.L(ctx, "[ProcessMaterialDownloading] workflow canceled", 
                "appVerID", appVer, "materialId", result.materialId)
            return nil
        }
        
        resToMatRefId[result.res.ID] = result.mat
        resourceBeDownload = append(resourceBeDownload, *result.res)
        linfo.L(ctx, "[ProcessMaterialDownloading] process material success", 
            "appVerID", appVer, "materialId", result.materialId, 
            "resToMatRefId", resToMatRefId)
    }
}

for 循环的次数等于素材数量,确保我们收集到所有结果

select 会等待任意一个 channel 有数据

如果 errChan 有数据,说明有错误发生,直接返回

如果 resultChan 有数据,处理结果并继续等待下一个结果

如果需要保持原有的顺序,可以这样:

go 复制代码
// 创建一个带缓冲的结果通道
resultChan := make(chan struct {
    res      *model.Resource
    mat      *model.Material
    materialId int64
    index    int
}, len(materialIds))

// 启动协程处理每个素材
for i, materialId := range materialIds {
    go func(mid int64, idx int) {
        matRes, matMaterial, mErr := ProcessMaterialDownloading(ctx, mid, workflow, appVer, matProc)
        if mErr != nil {
            errChan <- mErr
            return
        }
        resultChan <- struct {
            res      *model.Resource
            mat      *model.Material
            materialId int64
            index    int
        }{matRes, matMaterial, mid, idx}
    }(materialId, i)
}

// 使用 map 临时存储结果
tempResults := make(map[int]struct {
    res      *model.Resource
    mat      *model.Material
    materialId int64
})

// 等待所有协程完成
for i := 0; i < len(materialIds); i++ {
    select {
    case mErr := <-errChan:
        return mErr
    case result := <-resultChan:
        // 检查 workflow 状态
        curWorkflow, _ := db.W().GetWorkflow(ctx, workflow.ID)
        if curWorkflow.Status == enum.WorkflowStatus_Canceled {
            linfo.L(ctx, "[ProcessMaterialDownloading] workflow canceled", 
                "appVerID", appVer, "materialId", result.materialId)
            return nil
        }
        
        tempResults[result.index] = struct {
            res      *model.Resource
            mat      *model.Material
            materialId int64
        }{result.res, result.mat, result.materialId}
    }
}

// 按顺序处理结果
for i := 0; i < len(materialIds); i++ {
    result := tempResults[i]
    resToMatRefId[result.res.ID] = result.mat
    resourceBeDownload = append(resourceBeDownload, *result.res)
    linfo.L(ctx, "[ProcessMaterialDownloading] process material success", 
        "appVerID", appVer, "materialId", result.materialId, 
        "resToMatRefId", resToMatRefId)
}
相关推荐
奇树谦39 分钟前
Qt|槽函数耗时操作阻塞主界面问题
开发语言·qt
小羊斩肖恩42 分钟前
Go性能优化深度指南:从原理到实战
开发语言·性能优化·golang
晨非辰2 小时前
#C语言——学习攻略:深挖指针路线(三)--数组与指针的结合、冒泡排序
c语言·开发语言·数据结构·学习·算法·排序算法·visual studio
一只小风华~5 小时前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
苕皮蓝牙土豆5 小时前
Qt 分裂布局:QSplitter 使用指南
开发语言·qt
Brookty8 小时前
Java线程安全与中断机制详解
java·开发语言·后端·学习·java-ee
從南走到北9 小时前
JAVA东郊到家按摩服务同款同城家政服务按摩私教茶艺师服务系统小程序+公众号+APP+H5
android·java·开发语言·微信小程序·小程序
遇见尚硅谷9 小时前
C语言:20250728学习(指针)
c语言·开发语言·数据结构·c++·笔记·学习·算法
☆璇10 小时前
【C++】C/C++内存管理
c语言·开发语言·c++
愿你天黑有灯下雨有伞10 小时前
枚举策略模式实战:优雅消除支付场景的if-else
java·开发语言·策略模式