Go errors.Join 全是 nil 时为什么返回 nil
来源:17golang原创
时间:2026-10-04 14:44:38 284浏览 收藏
errors.Join 全是 nil 时返回 nil,原因很直接:这个函数会丢弃所有 nil 参数,再统计剩余的非 nil 错误。若计数为 0,就表示没有任何错误需要汇总,因此返回代表“没有错误”的 nil,而不是创建一个内容为空但本身非空的聚合错误。
这项约定让调用方仍然可以使用最普通的 if err != nil 判断。官方文档明确写明:Join 丢弃 nil,当所有输入都是 nil 时返回 nil。可核对 errors 标准库文档 与 join.go 源码。
先看四类输入的结果矩阵
这里不需要用纳秒来衡量,决定返回值的核心指标只有一个:输入中非 nil 错误的数量 n。
| 调用 | 非 nil 数量 n | 返回值 | 判断结果 |
|---|---|---|---|
errors.Join() | 0 | nil | err == nil |
errors.Join(nil, nil) | 0 | nil | err == nil |
errors.Join(errA, nil) | 1 | 非 nil 的聚合错误 | errors.Is(err, errA) |
errors.Join(errA, errB) | 2 | 非 nil 的聚合错误 | 可匹配两个子错误 |
package main
import (
"errors"
"fmt"
)
func main() {
errA := errors.New("A 失败")
// 空参数和全 nil 输入都没有可汇总的错误。
fmt.Println(errors.Join() == nil) // true
fmt.Println(errors.Join(nil, nil) == nil) // true
// 只要至少有一个非 nil 错误,结果就不再是 nil。
joined := errors.Join(errA, nil)
fmt.Println(joined == nil) // false
fmt.Println(errors.Is(joined, errA)) // true
}

源码先计数,n 为 0 就立即返回
标准库实现先遍历一次参数,计算非 nil 数量。这个计数同时承担两个作用:决定是否直接返回 nil,以及在确实有错误时为切片预留准确容量。
func Join(errs ...error) error {
n := 0
for _, err := range errs {
// 只有接口值本身不为 nil 才计入聚合结果。
if err != nil {
n++
}
}
if n == 0 {
// 没有真实错误时保留 Go 惯用的 nil 语义。
return nil
}
e := &joinError{errs: make([]error, 0, n)}
for _, err := range errs {
// 第二次遍历只复制非 nil 错误。
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
因此,errors.Join(nil, nil) 不会产生一个“空的 joinError”。如果它返回非 nil,上层代码就会把“没有子错误”误判成“发生了错误”,还要额外引入空集合的特殊分支。直接返回 nil 让组合函数与普通 Go 函数的错误契约保持一致。
批量收集错误时可以直接 return errors.Join
这个设计最适合多个独立检查:每个检查成功时返回 nil,失败时返回具体错误,最后统一合并。调用者不需要提前统计错误数量,也不需要为“切片为空”单独写返回分支。
func validateAll(values []string) error {
errs := make([]error, 0, len(values))
for _, value := range values {
if err := validate(value); err != nil {
// 只收集真实错误,保留每个失败原因。
errs = append(errs, err)
}
}
// 所有检查成功时自然返回 nil;有失败时返回聚合错误。
return errors.Join(errs...)
}
func validate(value string) error {
if value == "" {
return errors.New("值不能为空")
}
return nil
}
也可以把可能为 nil 的结果全部传入 Join,由它统一过滤。对于少量固定检查,这种写法尤其紧凑:
func checkConfig(cfg Config) error {
// 每个检查独立返回 error;Join 负责过滤成功项。
return errors.Join(
checkAddress(cfg.Address),
checkTimeout(cfg.Timeout),
checkToken(cfg.Token),
)
}
非 nil 结果是一棵可检查的错误树
当至少有一个非 nil 输入时,返回值实现 Unwrap() []error。errors.Is 和 errors.As 会检查错误树:先看当前错误,再按深度优先方式检查各个子错误。因此不应该依赖聚合错误与某个原始错误直接相等。
var ErrName = errors.New("名称无效")
var ErrPort = errors.New("端口无效")
func main() {
joined := errors.Join(ErrName, ErrPort)
// 聚合错误是包装对象,使用 errors.Is 检查子错误。
fmt.Println(errors.Is(joined, ErrName)) // true
fmt.Println(errors.Is(joined, ErrPort)) // true
// 直接相等只比较当前接口值,不会遍历错误树。
fmt.Println(joined == ErrName) // false
}

还有一个容易忽略的区别:errors.Unwrap 只调用形如 Unwrap() error 的方法,不会展开 Join 返回的 Unwrap() []error。要判断其中是否包含目标错误,直接使用 errors.Is 或 errors.As。
typed nil 不会被当成 nil 丢弃
Join 判断的是 error 接口值是否为 nil。如果接口里保存了一个类型信息非空、动态值为 nil 指针的错误,也就是常说的 typed nil,那么这个接口本身并不等于 nil,它会被当作一个非 nil 错误保留下来。
type MyError struct {
message string
}
func (e *MyError) Error() string {
// 真实项目应决定是否允许 nil 接收者,并安全处理。
if e == nil {
return "MyError(nil)"
}
return e.message
}
func main() {
var p *MyError
var err error = p
// 接口包含动态类型 *MyError,所以接口值并不为 nil。
fmt.Println(p == nil) // true
fmt.Println(err == nil) // false
fmt.Println(errors.Join(err) == nil) // false
}
如果该类型的 Error 方法不能处理 nil 接收者,之后格式化聚合错误时还可能发生 panic。更稳妥的做法是在返回 error 前避免把 nil 指针装进接口,而不是期待 errors.Join 替你识别 typed nil。
几个常见边界
只有一个非 nil 错误时,会原样返回它吗?
不会保证原样返回。当前标准库源码仍会创建 joinError,只是在格式化时直接使用唯一子错误的文本。判断身份应使用 errors.Is,不要依赖 joined == original。
嵌套 Join 会自动拍平成一层吗?
不会自动把内层 Join 的所有孩子复制到外层切片;内层聚合错误仍是外层错误树的一个子节点。不过 errors.Is 和 errors.As 会继续遍历内层节点,所以通常不需要手动拍平。
为什么不是返回一个空错误对象?
因为 Go 用 nil 表示成功。空错误对象虽然没有消息或子错误,但接口值仍然非 nil,会触发所有常规错误分支。返回 nil 才能让“没有任何失败”与普通函数的成功语义一致。
聚合错误的文本是什么格式?
每个非 nil 子错误的 Error() 文本按输入顺序拼接,彼此之间加入换行。文本适合展示,但程序逻辑应依据 errors.Is、errors.As 或明确的错误类型,而不是解析字符串。
结论
errors.Join 的判断标准可以压缩成一句话:过滤 nil 后,若非 nil 错误数量 n=0,返回 nil;若 n>0,创建实现 Unwrap() []error 的聚合错误。这个设计既保留了 Go 的成功语义,又让多个失败原因能够被 errors.Is 和 errors.As 继续识别。实际使用时只需额外留意 typed nil、直接相等比较和 errors.Unwrap 不展开多错误这三个边界。
-
157 收藏
-
Golang · Go问答 | 23分钟前 | 标准库 · HTTP服务 · 可观测性 · Go问答 · Go expvar http.ServeMux expvar.Handler /debug/vars 运行指标386 收藏
-
316 收藏
-
139 收藏
-
463 收藏
-
374 收藏
-
486 收藏
-
187 收藏
-
216 收藏
-
483 收藏
-
197 收藏
-
430 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习