Go errors.Join 全是 nil 时为什么返回 nil
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 不展开多错误这三个边界。
甲壳虫ADB助手支持智能手表吗?手表连接与设备管理边界
- 上一篇
- 甲壳虫ADB助手支持智能手表吗?手表连接与设备管理边界
- 下一篇
- PHP PDO 事务里执行 DDL 为什么可能自动提交
-
- Golang · Go问答 | 5分钟前 | flag · go · 初始化 · Go 命令行参数 init flag.Parse
- Go flag.Parse 为什么不能在 init 中提前调用
- 157浏览 收藏
-
- Golang · Go问答 | 23分钟前 | 标准库 · HTTP服务 · 可观测性 · Go问答 · Go expvar http.ServeMux expvar.Handler /debug/vars 运行指标
- Go expvar.Handler 与默认 /debug/vars 有什么区别
- 386浏览 收藏
-
- Golang · Go问答 | 50分钟前 |
- Go 自定义 Is 方法为什么不能递归调用 Unwrap
- 316浏览 收藏
-
- Golang · Go问答 | 1小时前 | 标准库 · Go教程 · 输入校验 · Go encoding/xml xml.Decoder strict XML容错
- Go xml.Decoder.Strict 关闭后会容忍哪些格式问题
- 139浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · encoding/json ·
- Go json.Decoder.UseNumber 为什么能避免浮点精度变化
- 463浏览 收藏
-
- Golang · Go问答 | 2小时前 | 序列化 · Go问答 · 错误排查 · Go encoding/gob register 类型未注册 接口字段 RegisterName
- Go gob 解码接口字段为什么提示类型未注册
- 374浏览 收藏
-
- Golang · Go问答 | 2小时前 | go · csv · Go encoding/csv csv.Reader LazyQuotes
- Go csv.Reader.LazyQuotes 会接受哪些非标准引号
- 486浏览 收藏
-
- Golang · Go问答 | 2小时前 |
- Go binary.Varint 返回负数读取长度是什么意思
- 187浏览 收藏
-
- Golang · Go问答 | 3小时前 |
- Go base64.DecodeString 报 CorruptInputError 怎么定位偏移
- 216浏览 收藏
-
- Golang · Go问答 | 4小时前 | Go问答 · Go 调试信息 Reader debug/dwarf SkipChildren DWARF Entry树
- Go dwarf.Reader.SkipChildren 什么时候必须调用
- 197浏览 收藏
-
- Golang · Go问答 | 4小时前 | Go问答 · Go runtime/debug BuildInfo.Settings ReadBuildInfo buildvcs VCS元数据
- Go BuildInfo.Settings 为什么可能缺少版本控制字段
- 430浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 325次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 384次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 376次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 343次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 167次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go代码规范错误处理示例经验总结
- 2022-12-23 278浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览

