错误链
Go 的错误链解决的是一个很具体的问题:函数需要给错误增加上下文,但上层仍要知道底层到底是“未找到”、权限失败,还是某个自定义类型。最小规则是:用 fmt.Errorf 的 %w 包装错误,用 errors.Is 判断固定错误,用 errors.As 提取类型。如果只是用 %v 拼进错误文本,日志看起来一样,程序却无法继续沿链判断。
官方参考:https://pkg.go.dev/errors
官方说明:https://go.dev/blog/go1.13-errors
%w保留被包装错误,形成可被遍历的错误链;%v只保留文本。errors.Is(err, target)适合判断哨兵错误,不能用字符串相等替代。errors.As(err, &target)适合取回自定义错误类型,目标变量要传地址。- 是否用
%w也是 API 设计决定:包装后,底层错误可能成为对调用方可见的契约。
先看触发信号:错误信息还在,类型判断却失效
值班时常见的现象是日志已经带上了足够的上下文,例如“读取用户配置失败:权限不足”,但代码中的 errors.Is 返回了 false,或者类型断言拿不到自定义错误。先不要改字符串比较,先沿着错误的创建位置检查包装动词。
下面两种写法打印出来都可能包含相同的错误信息,但它们对调用方暴露的关系不同:
package main
import (
"errors"
"fmt"
)
var ErrPermission = errors.New("permission denied")
func withTextOnly(err error) error {
// %v 只把底层错误格式化进文本,不建立 Unwrap 关系。
return fmt.Errorf("读取配置失败: %v", err)
}
func withChain(err error) error {
// %w 保留底层错误,调用方可以继续用 errors.Is 判断。
return fmt.Errorf("读取配置失败: %w", err)
}
func main() {
textErr := withTextOnly(ErrPermission)
chainErr := withChain(ErrPermission)
// 两个错误都能打印出上下文,但只有 chainErr 仍可匹配哨兵错误。
fmt.Println(errors.Is(textErr, ErrPermission)) // false
fmt.Println(errors.Is(chainErr, ErrPermission)) // true
}
快速判断可以记成一句话:%v 解决“人怎么看”,%w 同时解决“程序怎么认”。如果某个上层分支必须按底层错误采取不同动作,包装处就不能只追求可读文本。

用 %w 建立可继续判断的错误链
错误链的关键不是错误消息里出现了多少个冒号,而是每一层是否提供了 Unwrap() error 关系。标准库的 fmt.Errorf 在使用 %w 时会返回一个可以解包的错误;自定义错误类型也可以通过 Unwrap 把内部错误交给后续判断。
package main
import (
"errors"
"fmt"
)
var ErrPermission = errors.New("permission denied")
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
// Error 负责提供可读信息,不改变内部错误的身份。
return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error {
// Unwrap 把下一层错误暴露给 errors.Is 和 errors.As。
return e.Err
}
func loadUser() error {
// 先保存底层哨兵错误,再附加当前函数的业务上下文。
return &QueryError{Query: "select user", Err: ErrPermission}
}
func serviceCall() error {
err := loadUser()
if err != nil {
// %w 继续保留 QueryError 以及它内部的 ErrPermission。
return fmt.Errorf("用户服务调用失败: %w", err)
}
return nil
}
这里的链可以理解成“服务层包装 -> QueryError -> ErrPermission”。上层不需要手写循环调用 Unwrap;errors.Is 和 errors.As 会沿着这条关系检查。

固定错误用 errors.Is 判断,不要比较最终文本
哨兵错误适合代表一个稳定的程序分支,例如资源不存在、权限拒绝或输入结束。判断时把当前错误放在第一个参数,把目标哨兵放在第二个参数:
func isPermissionFailure(err error) bool {
// Is 会检查当前错误以及它通过 Unwrap 连接的每一层。
return errors.Is(err, ErrPermission)
}
func handle(err error) string {
if err == nil {
// nil 表示没有错误,先返回成功路径。
return "ok"
}
if errors.Is(err, ErrPermission) {
// 匹配成功后可以进入固定的权限处理分支。
return "请检查访问权限"
}
// 未识别的错误继续向上返回,保留原始上下文。
return "稍后重试"
}
不要写成 err.Error() == "permission denied"。一旦中间层增加了文件名、用户 ID 或操作名,文本就会变化;而错误链判断关注的是值或类型关系。也不要把 errors.Unwrap 当成主要业务判断工具,它只取一层,直接用 errors.Is 更能表达“链上是否存在该错误”。
自定义错误用 errors.As 取回结构化信息
如果上层需要读取 QueryError.Query 这样的字段,就用 errors.As。它不是把错误转换成字符串,而是在错误链中寻找目标类型,并把找到的值写入目标变量。
func queryName(err error) string {
var queryErr *QueryError
if errors.As(err, &queryErr) {
// As 成功后,queryErr 指向链中匹配到的 QueryError。
return queryErr.Query
}
// 没有该类型时不要假设字段存在,回到通用错误处理。
return "unknown query"
}
func classify(err error) string {
var queryErr *QueryError
if !errors.As(err, &queryErr) {
// 目标类型不存在时,保留统一的未知类别。
return "other"
}
if errors.Is(err, ErrPermission) {
// 同一个错误可以同时拥有类型信息和哨兵语义。
return "query-permission"
}
return "query-other"
}
传参时容易写错的是目标变量:目标是 *QueryError,因此要声明一个 *QueryError 变量,再把它的地址传给 errors.As。如果目标类型本身是值类型,则按实际类型声明,不要机械地再加一层指针。
回滚与 API 边界:包装不是越多越好
如果一次发布后发现调用方开始依赖某个底层错误,回滚时不能只把 %w 改回 %v。这会让已有的 errors.Is 或 errors.As 判断突然失效,属于行为变化。更稳妥的处理是先确认调用方是否已经把该错误当作契约,再决定是否调整错误类型或提供自己的稳定哨兵。
| 场景 | 推荐做法 | 需要注意 |
|---|---|---|
| 本包内部记录上下文 | 使用 %w 便于内部定位 | 检查是否把依赖错误泄露给公共 API |
| 公共函数返回稳定业务状态 | 暴露自有哨兵或自有类型 | 底层库替换时保持上层判断不变 |
| 只需要日志文本 | 可以使用 %v | 调用方不能再依赖该底层错误 |
| 需要字段处理 | 定义 Unwrap 并用 errors.As | 目标类型要传地址 |
官方错误处理说明特别强调:一旦包装某个底层错误,调用方就可能把它视为 API 的一部分。因此“是否用 %w”不仅是写法选择,也是抽象边界选择。
上线前的错误链检查清单
- 出现固定错误分支时,是否使用
errors.Is,而不是比较字符串或只比较最外层值? - 需要读取字段时,是否用
errors.As,并按实际类型正确传入目标地址? - 每个自定义包装类型是否实现了返回内部错误的
Unwrap() error? - 包装公共错误前,是否确认底层类型不会无意中变成长期 API 承诺?
- 回滚包装方式时,是否把
errors.Is/errors.As的调用方一起纳入兼容检查?
常见问题
错误信息一样,为什么 errors.Is 结果不同?
错误文本只负责展示;只有通过 %w 或自定义 Unwrap 建立关系,errors.Is 才能沿链查找目标。
errors.As 为什么要传一个指针的地址?
As 需要把匹配到的错误值写入目标变量。目标如果是 *QueryError,就要传 &queryErr,这样调用成功后变量才会指向链中的具体错误。
所有错误都应该用 %w 吗?
不应该。若不希望调用方依赖某个底层实现,可以只保留文本,或返回由本包定义的稳定错误类型;关键是先明确 API 边界。
-
- Golang · Go问答 | 31分钟前 | 标准库 · HTTP客户端 · Go问答 · Go httptrace ClientTrace Got1xxResponse HTTP 1xx
- Go httptrace.ClientTrace Got1xxResponse 怎样记录一百级响应
- 475浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 983次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 986次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1105次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1076次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 916次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go types.Selection.Type 与 Obj.Type 为什么不总是相同
- 2026-11-10 501浏览
-
- Go flag.FlagSet ContinueOnError 怎么捕获帮助输出和解析错误
- 2026-11-05 501浏览
-
- Go netip.ParseAddr 为什么拒绝带端口字符串
- 2026-11-03 501浏览

错误包装
