golang,go,博客,开源,编程
errgroup
包详解Golang 的扩展并发库 golang.org/x/sync/errgroup
提供了对多协程任务进行管理和错误处理的便利功能。
与基础的 sync.WaitGroup
相比,errgroup.Group
在等待所有任务完成的同时,还会自动捕获第一个非 nil 错误并返回。如果通过 WithContext
创建 Group
,当任一子任务返回错误时,errgroup
会取消关联的 Context
,从而通知其他协程提前退出。
简言之,errgroup
封装了错误传播、上下文取消和并发控制等功能,使并发编程更加简洁易用。
使用 errgroup
时,首先需要创建一个 Group
实例。可以直接用零值初始化:var g errgroup.Group
,或者调用 errgroup.WithContext(ctx)
同时获取一个基于 ctx
派生的新 Context
。典型的用法是对每个并发任务调用 g.Go(func() error)
来启动 goroutine。Group.Go
方法内部会自动执行 WaitGroup.Add(1)
,并在函数返回时执行 Done()
。最后使用 g.Wait()
等待所有任务完成:若有任务返回了错误,Wait
会返回该第一个非 nil 错误,否则返回 nil。例如:
import (
"context"
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
"io/ioutil"
)
func main() {
// 创建带取消功能的 errgroup 和 Context
g, ctx := errgroup.WithContext(context.Background())
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
// 用于收集响应结果的切片
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // 捕获循环变量
g.Go(func() error {
// 在请求中使用 errgroup 的 Context,以支持取消
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body)
results[i] = string(data)
return nil
})
}
// 等待所有并发任务完成
if err := g.Wait(); err != nil {
fmt.Println("执行过程中发生错误:", err)
return
}
fmt.Println("所有请求完成,结果:", results)
}
此外,errgroup.Group
提供了并发限制的功能。可以使用 g.SetLimit(n)
设置最多同时运行的协程数,配合 g.TryGo(f)
在不超限时才启动任务。例如:
var g errgroup.Group
g.SetLimit(5) // 限制最多 5 个并发 goroutine
for i := 0; i < 10; i++ {
g.TryGo(func() error {
// 模拟任务
fmt.Println("任务执行")
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Println("执行出错:", err)
} else {
fmt.Println("所有任务成功完成")
}
并发执行多个 HTTP 请求并收集结果: 在多个 API 请求或微服务调用场景下,可以用 errgroup
并发发送请求,并在所有请求完成后统一处理结果。例如上面的代码示例即同时请求多个 URL,并把结果保存在 results
切片中。如果任意一个请求出错,Wait()
会返回错误(并在使用 WithContext
时取消其他请求)。
遇到错误立即取消: 如果需要在一旦发现错误就中止其他任务的执行,可通过 WithContext
创建带取消信号的组。在下面示例中,第一个任务在 2 秒后返回错误,errgroup
会取消关联的 ctx
,使第二个任务立刻退出等待并返回 ctx.Err()
:
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
// 第一个协程 2s 后返回错误
g.Go(func() error {
time.Sleep(2 * time.Second)
return fmt.Errorf("第一个任务失败")
})
// 第二个协程检查 ctx.Done()
g.Go(func() error {
select {
case <-ctx.Done():
fmt.Println("第二个任务被取消:", ctx.Err())
return ctx.Err()
case <-time.After(3 * time.Second):
fmt.Println("第二个任务完成")
return nil
}
})
if err := g.Wait(); err != nil {
fmt.Println("errgroup 返回错误:", err)
}
}
运行结果中会看到“第二个任务被取消”和错误信息。通过 WithContext
,一旦有任务返回非 nil 错误,关联的 Context
会立即被取消。
超时控制和上下文取消:errgroup
常与 Context
组合使用来实现超时或取消。例如,如果将父上下文设置超时,当时间到达限制后,所有正在等待的协程会收到取消信号。下面示例为两个任务设置 2 秒超时,其中一个任务故意睡眠 3 秒,会被取消:
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
baseCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
g, ctx := errgroup.WithContext(baseCtx)
// 长任务:需 3 秒
g.Go(func() error {
time.Sleep(3 * time.Second)
return nil
})
// 观察超时信号
g.Go(func() error {
select {
case <-ctx.Done():
fmt.Println("任务取消:", ctx.Err())
return ctx.Err()
}
})
if err := g.Wait(); err != nil {
fmt.Println("errgroup 返回:", err)
}
}
当超时触发时,第二个任务检测到 ctx.Done()
并返回 context.DeadlineExceeded
。使用 WithContext
和超时上下文可以很方便地控制整个任务组的执行期限。
for
循环中直接使用迭代变量启动 goroutine 时,会导致闭包捕获同一个变量,结果通常不符合预期。正确做法是循环内部重新声明变量(例如 v := v
)或作为函数参数传入,避免所有协程共享同一个循环变量。例如:
for _, url := range urls {
url := url // 正确捕获
g.Go(func() error {
// 使用 url
return nil
})
}
errgroup.WithContext(ctx)
创建组时,不需要也不应再对同一个 ctx
额外调用 context.WithCancel
等,否则可能导致取消信号影响到外层逻辑。正确的做法是直接传入已有的上下文,WithContext
会返回派生的新 Context
和关联的取消函数。Group.SetLimit(n)
限制的是组中 持有 协程的数量(包括正在运行和在队列等待的),误用场景下容易出现循环依赖而死锁。尤其当在同一个 errgroup
中嵌套创建更多任务或组时,很可能死锁。最保险的方式是避免嵌套使用同一个 errgroup
实例。另外,将 SetLimit(0)
视为无效设置,会直接导致死锁,因为组里无法再启动任何协程。因此应谨慎使用并发限制,并确保不会使 goroutine 无法继续执行。errgroup.Group
只返回第一个发生的非 nil 错误,后续出现的错误会被丢弃。同时,Group.Wait
会调用 sync.WaitGroup.Wait
,在返回前会 等待所有 子任务退出。这意味着即使遇到错误,其他未完成的任务也会继续运行(直到收到取消信号退出)。因此在实际使用时,需要对返回的错误进行及时检查,并在必要时主动响应取消信号,让 goroutine 提前结束。errgroup
vs sync.WaitGroup
的区别sync.WaitGroup
只能等待所有协程完成,不会传递错误;而 errgroup.Group
在调用 Wait()
时会返回第一个非 nil 错误。这样可以方便地在有任何子任务出错时处理错误而不必自行同步。errgroup
集成了 context.Context
,可以在子任务出错时自动取消其他任务;而 sync.WaitGroup
不涉及上下文和取消逻辑。借助 WithContext
,errgroup
能让并发任务响应父上下文的取消信号,避免浪费资源。errgroup
提供了 SetLimit
和 TryGo
等方法,可限制并发 goroutine 的数量。sync.WaitGroup
则只能由用户自行 Add
来记录数量,没有并发池或信号量概念。sync.WaitGroup
需要手动调用 Add
/Done
不同,errgroup.Group
封装了这些操作,只需调用 Go
即可启动任务。此外,errgroup
还可自动捕获子任务 panic
(将其封装为 PanicError
抛出),提高了并发代码的鲁棒性。在真实项目中,errgroup
常用于在业务逻辑中并行执行若干相互独立的子任务。
例如,在一个 HTTP 服务的处理函数中,可能需要并行调用多个后端服务或查询不同数据源,然后整合结果返回给客户端。
可以这样组织代码:
在请求对应的 Context
上创建一个 errgroup
,为每个独立任务调用 g.Go()
,并在最后通过 g.Wait()
收集结果或错误。如果某一任务出错,关联的 Context
会自动取消,其它任务会及时退出,从而避免不必要的工作。
举个🌰:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
g, ctx := errgroup.WithContext(ctx)
var resA, resB string
// 并行执行两个调用
g.Go(func() error {
data, err := serviceA(ctx, ...)
if err == nil {
resA = data
}
return err
})
g.Go(func() error {
data, err := serviceB(ctx, ...)
if err == nil {
resB = data
}
return err
})
if err := g.Wait(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Results: %s, %s", resA, resB)
}
总的来说,errgroup
使得 Go 并发编程更易于编写健壮的代码,在出现错误时能够优雅地停止不必要的工作。
Context
使用: 通常通过 g, ctx := errgroup.WithContext(ctx)
来创建任务组,确保在某个子任务出错或超时后能够取消其他协程。在每个子任务中,应监听 ctx.Done()
并及时返回,以响应取消信号。v := v
或函数参数的方式复制循环变量,避免闭包捕获陷阱。这能确保每个 goroutine 拥有正确的参数值。ctx.Err()
或 <-ctx.Done()
,以便在外部取消后能尽快退出,释放资源。例如,如果任务中有 I/O 操作,可以传递 ctx
给相关 API 来触发取消。g.Wait()
后,应该检查返回的错误并记录或上报。如果只关注第一个错误,就要明确日志体现是哪一个任务失败;如果需要所有错误信息,则可考虑自行在各个 goroutine 中汇集。总之,不要忽略 Wait()
的返回值。errgroup
实例: 每个任务组只应使用一个 errgroup.Group
实例,避免嵌套使用同一个组。若需要多个独立的并发组合任务,应创建多个组实例,以免发生资源冲突或死锁。error
的函数,使 g.Go()
调用清晰简洁。