go-multierror: 更方便的处理你的错误列表

首发公众号:mp.weixin.qq.com/s/8EVPiNbyn...

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

使用示例

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

go 复制代码
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

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

bash 复制代码
$ go run main.go                     
Errors occurred:
2 errors occurred:
        * step1 failed
        * step2 failed

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

定制错误格式

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

go 复制代码
if errs != nil {
    errs.ErrorFormat = func(e []error) string {
        return e[0].Error()
    }
    fmt.Println("Errors occurred:")
    fmt.Println(errs.Error())
}

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

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

bash 复制代码
$ go run main.go
Errors occurred:
step1 failed

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

错误兼容性

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

示例如下:

go 复制代码
if errs != nil {
    fmt.Println("Errors occurred:")
    fmt.Println(errs.Error())
    fmt.Println(errors.Unwrap(errs))
    fmt.Println(errors.Unwrap(errors.Unwrap(errs)))
}

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

bash 复制代码
$ 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 包默认不支持并发操作。有如下测试代码:

go 复制代码
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 详解」。

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

bash 复制代码
$ 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 中处理。

示例如下:

go 复制代码
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 并完成聚合操作。

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

bash 复制代码
$ 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 协议更加严格的开源协议,如果你的商业化软件使用并修改了其源码,则修改后的源码需要以同样协议进行开源。

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

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

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

延伸阅读

联系我

相关推荐
易协同低代码13 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo13 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志14 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..14 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects14 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰14 小时前
一些基础和问题解决
后端
陈随易15 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN15 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana15 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超15 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构