当前位置:首页 > 文章列表 > Golang > Go问答 > 错误链

错误链

来源:17golang原创 2026-11-11 17:02:38 0浏览 收藏

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 同时解决“程序怎么认”。如果某个上层分支必须按底层错误采取不同动作,包装处就不能只追求可读文本。

Go 中 fmt.Errorf 使用百分号 v 和百分号 w 时错误链可判断边界的对比说明图
图1:边界说明图,对比 %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 会沿着这条关系检查。

Go 错误链中底层错误、百分号 w 包装、Unwrap、errors.Is 和 errors.As 的静态结构说明图
图2:结构说明图,展示多层包装仍保留原始错误关系,不是运行截图。

固定错误用 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 边界。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
错误格式化
下一篇
错误格式化
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    983次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    986次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1105次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1076次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    916次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码