原文:https://medium.com/a-journey-with-go/go-multiple-errors-management-a67477628cf1
关于开发者使用Go遇到的最大挑战的年度调查报告中,错误管理是经常被争论和反复提起的话题。然而,当涉及到在并发环境中处理错误或为相同的 goroutine 组合多个错误时,Go 提供了很好的包,使多个错误的管理变得很容易
单个 goroutine,多个错误
例如,当您处理具有重试策略的代码时,将多个错误合并成一个非常有用。下面是一个基本的例子,其中我们需要收集生成的错误:
var data = []byte(
`a,b,c
foo
1,2,3
,",
`)
func main() {
reader := csv.NewReader(bytes.NewBuffer(data))
for {
if _, err := reader.Read(); err != nil {
if err == io.EOF {
break
}
log.Printf(err.Error())
}
}
}
上面的程序读取、解析一个 CSV 文件,并且显示报错信息。它可以更方便的对错误进行分组以获得完整的报告。为了将错误合并成一个,我们需要在两个很优秀的包中选择一个:
-
使用 HashiCorp 的
go-multierror
,可以将错误合并为一个标准错误func main() { var errs error reader := csv.NewReader(bytes.NewBuffer(data)) for { if _, err := reader.Read(); err != nil { if err == io.EOF { break } errs = multierror.Append(errs, err) } } if errs != nil { log.Printf(errs.Error()) } }
接着错误报告可以被打印出来
-
使用 Uber 的
multierr
代码的实现是类似的,下面是输出
错误通过分号连接,没有任何其他格式。
关于每个包的性能,下面是一个具有较高失败次数的程序的基准测试
name time/op alloc/op allocs/op HashiCorpMultiErrors-4 6.01µs ± 1% 6.78kB ± 0% 77.0 ± 0% UberMultiErrors-4 9.26µs ± 1% 10.3kB ± 0% 126 ± 0%
Uber 的速度稍慢,占用内存更多。但是,这个包的设计目的是在收集到错误之后将其分组,而不是每次都添加错误。当对错误进行分组时,结果很接近,但是代码不够优雅,因为它需要额外的步骤。以下是新的测试结果:
name time/op alloc/op allocs/op HashiCorpMultiErrors-4 6.01µs ± 1% 6.78kB ± 0% 77.0 ± 0% UberMultiErrors-4 6.02µs ± 1% 7.06kB ± 0% 77.0 ± 0%
这两个包都利用了 Go 错误接口,它们都实现了自己的
Error() string
函数。
一个错误, 多个 goroutines
当多个 goroutine 来处理一个任务时,正确管理结果和聚合错误以确保程序的正确性是必要的。
让我们从一个使用多个 goroutine 执行一系列操作的程序开始,每一个操作都持续一秒:
func main() {
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := action(); err != nil {
return
}
if err := action(); err != nil {
return
}
if err := action(); err != nil {
return
}
}()
}
wg.Wait()
}
为了解释错误的传播,第三个 goroutine 的第一个操作会失败,下面时正在发生的事情:
正如预期的那样,程序大约需要 3 秒钟,因为大多数 goroutine 需要经历三个操作,每个操作花费一秒:
go run . 0.30s user 0.19s system 14% cpu 3.274 total
然而,我们可能希望让这些 goroutine 互相依赖,并在其中一个失败时取消它们以避免不必要的工作,解决方案是添加一个上下文,一旦 goroutine 失败,它就会取消它。
这个功能这是 errgroup
包所提供的;当一组 goroutine 工作时的错误和上下文传播。
下面时使用 errgroup
包的代码:
func main() {
g, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 4; i++ {
g.Go(func () error {
if err := action(ctx); err != nil {
return err
}
if err := action(ctx); err != nil {
return err
}
if err := action(ctx); err != nil {
return err
}
return nil
})
}
if err := g.Wait(); err != nil {
log.Printf(err.Error())
}
}
现在程序运行的更快了,因为它在发生错误时传播了取消上下文:
go run . 0.30s user 0.19s system 38% cpu 1.269 total
该包提供的另一个好处是,我们不再需要担心 goroutine 的添加和对 goroutine 标记完成。包替我们管理这些, 我们只需要等待程序完成并结束。