← 中文资源

简体中文版本

Errgroup 限流并行扇出

用 errgroup 的 SetLimit 限制扇出并发:分片失败不取消全局、成功结果照常收集,全部失败才整体报错。

阅读英文原文

示例说明

扇出调用的策略抉择:一票否决还是容忍部分失败。聚合检索场景里单个分片过载不该废掉整个查询——任务函数吞掉错误记入失败列表并返回 nil,errgroup 不触发取消,其余分片照常完成;只有全部分片阵亡才判定整体失败。SetLimit 把瞬时并发压在阈值内,分片数再大也不会压垮下游。

技术要点

  • SetLimit 上限内排队执行
  • 部分失败语义聚合成功结果
  • 全失败才返回整体错误

代码解读

fanout 的闭包里失败进 failures 切片、成功进 results,互斥锁保护两个切片的追加;闭包恒返回 nil 保证 errgroup 不取消兄弟任务。Wait 后按失败数判定:全败返回错误,否则返回成功的分片结果。示例的六分片里三号失败,五个成功结果照常返回。

使用建议

错误吞掉前要记录日志与指标。调用方需要知道部分失败时把 failures 一并返回。限流值与下游分片配额对齐。

代码示例

package main

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"golang.org/x/sync/errgroup"
)

type ShardResult struct {
	ShardID int
	Rows    []string
}

func fetchShard(ctx context.Context, shard int) ([]string, error) {
	if shard == 3 {
		return nil, errors.New("shard 3 overloaded")
	}
	return []string{
		fmt.Sprintf("shard-%d-row-a", shard),
		fmt.Sprintf("shard-%d-row-b", shard),
	}, nil
}

func fanout(ctx context.Context, shards, concurrency int) ([]ShardResult, error) {
	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(concurrency)

	var mu sync.Mutex
	results := make([]ShardResult, 0, shards)
	var failures []error

	for shard := 0; shard < shards; shard++ {
		g.Go(func() error {
			rows, err := fetchShard(ctx, shard)
			if err != nil {
				mu.Lock()
				failures = append(failures, fmt.Errorf("shard %d: %w", shard, err))
				mu.Unlock()
				return nil // 单分片失败不取消全局,继续收其余结果
			}
			mu.Lock()
			results = append(results, ShardResult{ShardID: shard, Rows: rows})
			mu.Unlock()
			return nil
		})
	}

	_ = g.Wait()
	if len(failures) == shards {
		return nil, fmt.Errorf("all shards failed: %v", failures)
	}
	return results, nil
}

func main() {
	results, err := fanout(context.Background(), 6, 3)
	fmt.Println("shards succeeded:", len(results), "err:", err)
}