当前位置:首页 > 文章列表 > Golang > Go教程 > Go errors.Join 怎么保留多个校验错误并让调用方逐个判断

Go errors.Join 怎么保留多个校验错误并让调用方逐个判断

来源:17golang原创 2026-09-07 21:39:17 0浏览 收藏

一次校验可能同时发现“姓名为空”和“年龄超范围”。如果只拼接错误字符串,调用方只能看到一段文案,无法稳定判断到底包含哪一类问题。Go 1.20 起可以用 errors.Join 保留多个错误,再用 errors.Is 分别判断;只有需要把每个字段提示单独列出来时,才读取 Unwrap() []error

把错误值留给控制流,把错误文本留给展示层:每个校验项先保留自己的原因,在函数出口统一 errors.Join;调用方用 errors.Is 做类别判断,不要比较 err.Error()

速记

  • errors.Join 会忽略 nil;所有输入都是 nil 时返回 nil。
  • 字段上下文用 Unwrap() error 包住哨兵错误,才能继续被 errors.Is 识别。
  • Joined error 的文本会按错误字符串换行拼接,但文本不是稳定的业务协议。
  • 需要逐条展示时读取 Unwrap() []error,普通分类判断不必手动遍历。

先让每个校验错误保留自己的原因

先定义稳定的错误原因,再给原因加字段名。下面的 FieldError 负责给人看的上下文,Unwrap 负责把语义交还给标准库:

package validation

import (
	"errors"
	"fmt"
	"strings"
)

var (
	ErrRequired = errors.New("required") // 表示字段没有填写
	ErrRange    = errors.New("out of range") // 表示字段值超出业务范围
)

type FieldError struct {
	Field string
	Cause error
}

func (e *FieldError) Error() string {
	return fmt.Sprintf("%s: %v", e.Field, e.Cause) // 文案带字段,语义仍由 Cause 保留
}

func (e *FieldError) Unwrap() error { return e.Cause }

func ValidateProfile(name string, age int) error {
	var problems []error
	if strings.TrimSpace(name) == "" {
		problems = append(problems, &FieldError{Field: "name", Cause: ErrRequired}) // 收集而不是立刻返回
	}
	if age  120 {
		problems = append(problems, &FieldError{Field: "age", Cause: ErrRange}) // 保留年龄错误类别
	}
	return errors.Join(problems...)
}

FieldError.Error 只负责形成可读文本,Unwrap 才是可判断的链路。这样 errors.Is 会沿着 joined error 的错误树继续查找,字段名不会因为增加上下文而丢失。

校验输入、哨兵错误、FieldError 与 errors.Join 的静态关系图
图1:字段错误保留原始原因后汇入 errors.Join,展示层与判断层都能继续使用。

在函数出口统一调用 errors.Join

收集切片的好处是每项校验都能完成,调用方一次就能拿到完整结果。errors.Join 会丢弃 nil;如果没有任何非 nil 错误,返回值就是 nil,因此不需要额外维护一个“是否失败”的布尔值。

它还保留加入顺序来生成展示文本,每个子错误的 Error() 结果之间用换行连接。不过这只是默认展示格式,不应被当成解析协议。字段级提示若需要本地化、排序或去重,应在展示层按自己的规则处理。

如果只有一个校验失败,也不要为了追求“单错误类型”而把它转成字符串。joined error 仍然可以被 errors.Is 判断;上层只需要依赖 error 接口的语义,而不是具体实现类型。

调用方如何分别判断每一种校验错误

调用方通常只关心错误类别,不需要知道内部收集了几个字段。对每个稳定原因调用一次 errors.Is 即可:

err := validation.ValidateProfile("", 150)
if err != nil {
	if errors.Is(err, validation.ErrRequired) {
		show("请填写姓名") // 按错误类别给出明确提示
	}
	if errors.Is(err, validation.ErrRange) {
		show("年龄应在 18 到 120 之间") // 另一个类别可以同时命中
	}
}

这里的两个判断可以同时为真,因为它们对应同一个 joined error 中的两个分支。不要改成 switch err.Error(),也不要用 err == validation.ErrRequired:前者依赖文案,后者无法穿过 FieldErrorerrors.Join

如果还要给日志加操作上下文,继续用 fmt.Errorf("validate profile: %w", err) 包装。只要使用 %w,外层判断仍然能到达内部错误树。

joined error 通过 errors.Is 分类判断并通过 Unwrap 列出字段提示的关系图
图2:调用方用 errors.Is 做类别判断,需要逐条展示时再读取 Unwrap() []error。

逐条展示时再读取 Unwrap() []error

标准库的 errors.Unwrap 适合单个 Unwrap() errorerrors.Join 返回的非 nil 错误实现的是 Unwrap() []error,所以展示每一项时应显式声明这个小接口:

type multiUnwrapper interface {
	Unwrap() []error // 只读取 Join 暴露的子错误,不依赖具体私有类型
}

func messages(err error) []string {
	var joined multiUnwrapper
	if !errors.As(err, &joined) {
		return []string{err.Error()} // 普通单错误仍有可读兜底
	}

	items := make([]string, 0, len(joined.Unwrap()))
	for _, item := range joined.Unwrap() {
		if item != nil {
			items = append(items, item.Error()) // 展示层收集字段级文案
		}
	}
	return items
}

这段遍历只适合展示、统计或附加字段上下文。若子错误还可能嵌套另一个多错误,需先明确产品是否要递归展开;很多表单场景只展示 Join 的第一层就够了。控制流判断仍优先写 errors.Iserrors.As,避免让业务代码绑定到某个实现细节。

用三组结果检查错误语义

最小测试矩阵至少覆盖三种输入:姓名和年龄都错、只错一项、全部正确。断言应优先检查 errors.Is 和 nil,不要把整段换行文本当成唯一断言:

  • 多错:errors.Is(err, ErrRequired)errors.Is(err, ErrRange) 都为 true。
  • 单错:对应类别为 true,另一类别为 false,展示列表只有一项。
  • 全通过:err == nil,不再调用展示函数。
  • 包装后:外层用 %w 增加上下文,两个类别判断仍然成立。

常见误区是把 nil 过滤、错误去重和文案排序混在校验函数里。errors.Join 只负责组合错误,不会自动去重;同一原因被加入两次时,errors.Is 仍然只是命中,但默认文本可能重复。是否去重应由业务决定,且不要通过拆解错误字符串实现。

相关问题

errors.Join 需要 Go 哪个版本?

它从 Go 1.20 加入标准库;项目若要兼容更早版本,需要先确认工具链约束,不能只替换 import 就假设行为一致。

为什么不用 strings.Join 拼错误消息?

字符串只能给人看,不能保留哨兵错误和包装关系。组合后需要分类处理时,应保留 error 值,再单独生成展示文案。

什么时候不该手动读取 Unwrap() []error?

只做“是否包含某类错误”的判断时不需要读取它;只有逐项展示、统计或需要字段上下文时,才在边界处遍历。

落地时可以记住一条线:字段校验返回带上下文的原始错误,函数出口用 errors.Join 收口,调用方用 errors.Is 分别判断,展示层再决定是否展开子错误。这样增加提示文案不会破坏业务分支。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go signal.Notify 没收到 SIGTERM 时要检查哪些边界Go signal.Notify 没收到 SIGTERM 时要检查哪些边界
上一篇
Go signal.Notify 没收到 SIGTERM 时要检查哪些边界
MySQL 组合索引列顺序怎么配合范围条件和排序
下一篇
MySQL 组合索引列顺序怎么配合范围条件和排序
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    173次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    106次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    33次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    42次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    78次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码