go-multierror 是一个第三方的 Go 语言库,用于处理多个错误的聚合与管理。它由 HashiCorp 提供,非常适合需要在某些操作中收集多个错误并在最后统一返回的场景。

使用示例

顾名思义,go-multierror 包的核心功能就一个,将多个错误合并为一个错误。以下是一个典型的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"errors"
"fmt"

"github.com/hashicorp/go-multierror"
)

func main() {
var errs *multierror.Error

// 模拟多个操作可能失败
if err := step1(); err != nil {
errs = multierror.Append(errs, err)
}

if err := step2(); err != nil {
errs = multierror.Append(errs, err)
}

// 如果有任何错误,将返回聚合的错误
if errs != nil {
fmt.Println("Errors occurred:")
fmt.Println(errs.Error())
} else {
fmt.Println("All steps succeeded!")
}
}

func step1() error {
return errors.New("step1 failed")
}

func step2() error {
return errors.New("step2 failed")
}

示例中,我们定义了一个 *multierror.Error 类型的 errs 变量,它提供了 Append 方法可以为其追加 error。当 step1step2 都失败时,errs 中就记录了这两个 error

执行示例代码,得到输出如下:

1
2
3
4
5
$ go run main.go                     
Errors occurred:
2 errors occurred:
* step1 failed
* step2 failed

可以看到,*multierror.Error 可以输出错误数量 2,以及其包含的每个 error 信息。

定制错误格式

我们还可以自定义错误的输出格式,修改上文示例中的 if errs != nil 代码块中的逻辑,将 errs.ErrorFormat 属性重新赋值为自定义函数:

1
2
3
4
5
6
7
if errs != nil {
errs.ErrorFormat = func(e []error) string {
return e[0].Error()
}
fmt.Println("Errors occurred:")
fmt.Println(errs.Error())
}

这个匿名函数接收一个 error 列表,并返回 error 列表中的第一个错误信息。

执行示例代码,得到输出如下:

1
2
3
$ go run main.go
Errors occurred:
step1 failed

基于此你可以定制任何自己想要的格式。

错误兼容性

go-multierror 包兼容了 errors.Unwrap 方法,所以它可以实现错误解包操作。

示例如下:

1
2
3
4
5
6
if errs != nil {
fmt.Println("Errors occurred:")
fmt.Println(errs.Error())
fmt.Println(errors.Unwrap(errs))
fmt.Println(errors.Unwrap(errors.Unwrap(errs)))
}

执行示例代码,得到输出如下:

1
2
3
4
5
6
7
8
9
$ go run main.go
Errors occurred:
2 errors occurred:
* step1 failed
* step2 failed


step1 failed
step2 failed

事实上,go-multierror 包完全兼容 Go 内置的 error 操作,无论是错误断言 err.(*multierror.Error) 还是 errors.Iserrors.As 都可以支持,你可以自行尝试。

并发场景

遗憾的是,go-multierror 包默认不支持并发操作。有如下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func TestConcurrency(t *testing.T) {
var wg sync.WaitGroup
errs := &multierror.Error{}

for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
errs = multierror.Append(errs, errors.New("error"))
}()
}

wg.Wait()
// 预期 100 个错误,实际输出可能 < 100
assert.Equal(t, 100, len(errs.Errors)) // 测试失败
}

这里并发开启 100 个 goroutine,每个 goroutine 中追加一个 errorerrs 对象中,使用 sync.WaitGroup 等待并发操作完成。

NOTE:

如果你不熟悉 sync.WaitGroup,可以参考我的文章「Go 并发控制:sync.WaitGroup 详解」。

执行测试代码,得到输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ go test -v -run=^TestConcurrency$
=== RUN TestConcurrency
main_test.go:26:
Error Trace: /blog-go-example/error/go-multierror/main_test.go:26
Error: Not equal:
expected: 100
actual : 79
Test: TestConcurrency
--- FAIL: TestConcurrency (0.00s)
FAIL
exit status 1
FAIL github.com/jianghushinian/error/go-multierror 0.196s

可以看到,测试结果失败了,预期 100 个错误,实际只得到 79 个错误。说明在并发操作过程中有部分 error 丢失了。

要解决这个问题,我们可以使用 channel 来并发安全的发送 error 对象,然后将错误聚合操作放在一个 goroutine 中处理。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func TestConcurrencyWithChannel(t *testing.T) {
var wg sync.WaitGroup
errCh := make(chan error, 100)
errs := &multierror.Error{}

for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
errCh <- errors.New("error") // 通过 channel 安全发送 err
}()
}

// 开启子 goroutine 等待并发程序执行完成
go func() {
wg.Wait()
close(errCh)
}()

// main goroutine 从 channel 收到 err 并完成聚合
for err := range errCh {
errs = multierror.Append(errs, err) // 单 goroutine 聚合,无竞争
}

// 预期 100 个错误,实际输出也是 100
assert.Equal(t, 100, len(errs.Errors)) // 测试通过
}

我们对上文中的测试代码进行了改造,在所有子 goroutine 中不再直接追加 errorerrs 对象中,而是先将其发送到带有缓冲的 channel 中,最终由 main goroutine 从 channel 收到 err 并完成聚合操作。

执行修改后的测试代码,得到输出如下:

1
2
3
4
5
$ go test -v -run=^TestConcurrencyWithChannel$
=== RUN TestConcurrencyWithChannel
--- PASS: TestConcurrencyWithChannel (0.00s)
PASS
ok github.com/jianghushinian/error/go-multierror 0.403s

这一次测试成功了,说明我们使用 channel 的方式解决了 go-multierror 包的并发问题。

常用方法

go-multierror 包常用方法总结如下:

  1. multierror.Append(*Error, error):向 multierror.Error 对象添加一个新错误。如果传入的 multierror.Errornil,会自动创建新的实例。
  2. *Error.Error():将所有错误格式化为字符串,按序列号展示。
  3. *Error.ErrorOrNil():如果没有错误,返回 nil。否则,返回聚合后的错误。

使用场景

最后我们再来探讨下 go-multierror 的常见使用场景,我认为有如下场景比较适合使用 go-multierror

  • 批量操作:当程序需要对一组任务(如文件处理、并发请求等)逐个执行,但每个任务可能独立失败时,使用 multierror 可以方便地记录并返回所有失败信息。此时的你是否想起了 errgroup 呢?
  • 资源清理:当程序释放多个资源时(比如执行多个 defer),若某些清理操作失败,可以收集这些错误并统一报告。
  • 复杂流程错误管理:在长流程中,允许多个步骤分别记录错误,而不是只返回第一个错误。

总结

go-multierror 非常简单易用,它适用于需要同时管理多个错误的场景。并且完全兼容 Go error,所以也非常实用。总之,go-multierror 是一个小巧而实用的错误管理工具,特别适合在复杂场景中对多个错误进行统一处理和报告。

不过最后我想额外提一点,go-multierror 项目采用 MPL-2.0 license 开源协议,这个协议是比 Apache 2.0 协议更加严格的开源协议,如果你的商业化软件使用并修改了其源码,则修改后的源码需要以同样协议进行开源。

下图是我制作的常见开源协议一览图,从左到右限制越来越宽松,如果你感兴趣也可以阅读我的另一篇文章「开源协议简介」。

licence
licence

本文示例源码我都放在了 GitHub 中,欢迎点击查看。

希望此文能对你有所启发。

延伸阅读

联系我