go 中 error 接口不支持直接叠加多个错误,因单个 error 值仅表达一种失败状态;需用 errors.join(go 1.20+)合并非 nil 错误或自定义实现 error 接口的结构体来承载上下文与结构化信息。

Go 中的多重错误:为什么 error 不能直接“叠加”
Go 的 error 类型是接口,单个值只能表达一种失败状态。你无法像 Python 那样用 raise ExceptionGroup 一次抛出多个错误,也不能靠 return err1, err2 —— Go 函数签名不支持返回两个 error 类型值(除非你手动定义结构体或切片)。常见误操作是写成:
func badExample() (int, error, error) { ... }
这不仅破坏可读性,还让调用方必须记住每个 error 的语义位置,且无法被标准错误处理逻辑(如 if err != nil)自然覆盖。
用 errors.Join 合并多个错误(Go 1.20+)
errors.Join 是官方推荐的多重错误聚合方式,它返回一个实现了 error 接口的新错误值,内部按顺序保存所有非 nil 错误,并在 Error() 方法中拼接描述。适用于需要“汇总失败原因”的场景,比如批量操作中多个子任务出错。
- 只合并
non-nil错误;传入nil会被忽略 - 结果错误本身可参与标准判断:
if err != nil成立当且仅当至少一个输入非 nil - 可用
errors.Is或errors.As检查其中是否包含某个特定错误类型 - 注意:
errors.Join不保证顺序绝对稳定(底层用 map 遍历),但实际中按参数顺序拼接字符串
示例:
立即学习“go语言免费学习笔记(深入)”;
err1 := fmt.Errorf("failed to open file A")
err2 := fmt.Errorf("failed to open file B")
combined := errors.Join(err1, err2)
fmt.Println(combined.Error()) // "failed to open file A; failed to open file B"
自定义错误容器:当需要保留上下文或结构化信息时
如果错误需要携带额外字段(如失败项的索引、重试次数、原始 HTTP 状态码),或者你想控制格式化逻辑(比如换行显示、JSON 序列化),就得自己实现 error 接口。这时不要用 errors.Join,而是定义结构体:
type MultiOperationError struct {
OpName string
Errors []error
}
func (e *MultiOperationError) Error() string {
var b strings.Builder
b.WriteString(fmt.Sprintf("[%s] %d failures: ", e.OpName, len(e.Errors)))
for i, err := range e.Errors {
if i > 0 { b.WriteString("; ") }
b.WriteString(err.Error())
}
return b.String()
}
关键点:
- 必须实现
Error() string方法才能满足error接口 - 若需支持
errors.As或errors.Is,还需实现Unwrap() []error(返回e.Errors) - 避免在
Error()中做耗时操作(如网络请求、磁盘读取)
配合多重返回值设计:别把错误“塞进”业务值里
Go 函数常返回 (T, error),但遇到多个可能失败的业务值(如 (string, int, error)),容易让人困惑哪个值在 error != nil 时有效。正确做法是:
- 只返回一个业务结果(哪怕是个结构体),把所有相关数据打包进去
- 错误始终作为最后一个返回值,且只一个
error类型 - 若某字段“可能不存在”,优先用指针或
optional模式(如*string),而不是靠错误类型区分 - 不要为了“省一个 error”而返回
(T, bool)并用 bool 表示失败 —— 这绕过了 Go 的错误约定,调用方无法用errors.Is做统一处理
反例:
func badAPI() (string, int, bool) { ... } // 调用方得记住第三个是 success flag
正例:
type Result struct {
Name string
Code int
}
func goodAPI() (Result, error) { ... }
真正难处理的不是语法层面怎么写,而是决定哪些错误该合并、哪些该短路返回、哪些该包装为新错误类型——这取决于你的 API 边界和调用方预期。别为了“看起来更 Go”而放弃语义清晰。










