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