← 中文资源

简体中文版本

Errgroup 上下文取消并行请求

用 errgroup 加派生 context 并行抓取多个 URL:首个错误自动取消兄弟任务,Wait 汇聚结果,goroutine 必然回收。

阅读英文原文

示例说明

并行任务组最大的风险是失控:一个任务出错后其余任务还在白跑,或者 panic 后 goroutine 泄漏。errgroup.WithContext 把两者一次解决——派生的 context 在首个错误发生时被取消,所有用该 context 的任务感知后提前退出;Wait 等全部子任务结束后返回那个首个错误,结果数组按索引写入天然无竞争。

技术要点

  • WithContext 派生可取消的分组上下文
  • 首个错误触发全组取消
  • 预分配结果数组按索引写避免锁

代码解读

fetchAll 循环里显式捕获循环变量(旧版 Go 的经典陷阱);每个任务的请求都带分组 context,取消即刻中断网络 IO。结果写入预分配数组的对应下标,无需互斥锁。Wait 返回非空错误时说明至少一个抓取失败,其余已被取消。

使用建议

新版 Go 循环变量语义已改,捕获写法可省但保留无害。任务内 panic 不会自动恢复,需要时用 g.Go 包装 recover。限制并发可换 errgroup 的 SetLimit。

代码示例

package main

import (
	"context"
	"fmt"
	"net/http"

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

type Result struct {
	URL  string
	Code int
}

func fetchAll(ctx context.Context, urls []string) ([]Result, error) {
	g, ctx := errgroup.WithContext(ctx)
	results := make([]Result, len(urls))

	for i, url := range urls {
		i, url := i, url // 捕获循环变量(Go 1.22 前必需)
		g.Go(func() error {
			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
			if err != nil {
				return fmt.Errorf("build request %s: %w", url, err)
			}
			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				return fmt.Errorf("fetch %s: %w", url, err)
			}
			defer resp.Body.Close()
			results[i] = Result{URL: url, Code: resp.StatusCode}
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err // ctx 已取消,其余 goroutine 全部提前退出
	}
	return results, nil
}

func main() {
	rs, err := fetchAll(
		context.Background(),
		[]string{"https://example.com/a", "https://example.com/b"},
	)
	fmt.Println(len(rs), err)
}