登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go errors.Is 与 errors.As 设计可判断错误链

来源:17golang原创

时间:2026-10-16 09:32:11 474浏览 收藏

Go 设计可判断错误链时,可以用固定哨兵错误表达业务类别,用自定义错误类型携带字段,再通过 Unwrap 和 fmt.Errorf 的 %w 保留关系。调用方用 errors.Is 判断“属于哪一类”,用 errors.As 取得“哪种结构以及哪些字段”,不必解析错误字符串。

本文以导出任务的配额判断为例,比较不同表达方式的契约与代价,不涉及云服务选型,也不把错误文本当成稳定协议。官方文档入口:https://pkg.go.dev/errors;包装设计说明:https://go.dev/blog/go1.13-errors;格式化接口:https://pkg.go.dev/fmt#Errorf。

先明确调用方需要作出什么判断

假设导出服务需要拒绝超过配额的请求。页面层只想知道是否提示调整数量,诊断层还要知道请求量与上限,服务层则希望补充“准备导出时失败”的上下文。把这三种需求全塞进一条字符串,会让修改措辞变成调用方兼容性风险。

本例把需求限定为两个可判断点:ErrQuota 表示配额超限;QuotaError 携带 Limit 和 Requested。planExport 负责添加操作上下文,但保留已承诺的业务错误能力。它不读取真实账号或套餐,也不执行导出,数字只是用于说明返回结构。

文字、哨兵和错误类型分别承担什么

表达方式适合承诺什么需要承担的代价
普通错误文本为日志提供可读说明不能让调用方依赖固定措辞分支
哨兵错误稳定的业务类别需要统一使用同一个值并说明返回场景
自定义错误类型字段、类型或能力导出字段与类型形态成为兼容约定
包装错误增加上下文且保留内部可判断能力内部错误可能成为外部可观察契约

Is 不是错误文本比较器。errors.New 即使传入相同字符串,也会创建不同的错误值。As 也不是从字符串中解析字段;它沿可解包关系寻找匹配对象,把对象赋给目标变量。直接类型断言只看当前错误值,遇到外层包装就可能不匹配。

哨兵与错误类型不是二选一。只需要分类时没有必要为每个情况建立结构体;确实需要上限、字段名或操作信息时,自定义类型才更有价值。这个案例需要两者,所以让 QuotaError 包含 ErrQuota。

保留一条可解包的业务错误链

完整示例可以保存为 main.go,由读者自行运行。checkQuota 负责判断,planExport 负责包装,main 展示消费方式。本文没有执行代码,后面的结果描述均为按逻辑推导的预期。

package main

import (
    "errors"
    "fmt"
)

// 同一业务类别共用这一个值,不在每次调用时重新创建
var ErrQuota = errors.New("quota exceeded")

type QuotaError struct {
    Limit int
    Requested int
    cause error
}

func (e *QuotaError) Error() string {
    // 文本给人阅读,程序不要解析这里的措辞
    return fmt.Sprintf("requested %d, limit %d", e.Requested, e.Limit)
}

func (e *QuotaError) Unwrap() error {
    return e.cause // 让调用方仍能判断业务类别
}

func checkQuota(requested, limit int) error {
    if requested 

链的外层是带操作说明的包装错误,内部是 QuotaError,再内部是 ErrQuota。QuotaError 的 Error 方法只负责文本,Unwrap 才是保留内部关系的入口。如果只在 Error 中拼接内部错误内容,却不提供包装关系,Is 无法凭那些文字找到哨兵。

外层包装、结构化错误与哨兵类别的静态包含关系
图1:原创静态说明图。操作上下文与业务错误分组说明各对象的归属和包含关系,不是执行流程或实测输出。

checkQuota 成功时返回真正的 nil,planExport 只在 err 非 nil 时包装。不要用一个内部指针为 nil 的错误接口冒充成功,也不要在成功路径上无条件 fmt.Errorf。一个错误接口是否为 nil,取决于其动态类型和值,而不只是内部指针。

调用方同时取得类别与结构化信息

以请求 5、上限 3 为输入,预期 quota 为 true,As 得到 Requested=5、Limit=3。新建的同文案 errors.New 不应匹配原哨兵,textOnly 也不应保留 ErrQuota 的判断能力。这些对照用于说明契约,不表示真实业务配额或运行测量。

var detail *QuotaError 后传入 &detail 容易让初学者犹豫:这里需要的是“目标变量的地址”。detail 本身是一个等待被赋值的 *QuotaError,As 需要能够修改这个变量,因此传入其地址。匹配成功后才读取 detail 的字段;不要忽略布尔返回值。

本例的 Error 使用指针接收者,所以 *QuotaError 实现 error。把 target 写成不匹配的值类型并不会自动变成同一种类型;不合法的 target 还可能引发 panic。最好让返回类型、接收者和目标变量三者保持一致。

同一错误对象分别供类别判断和字段提取使用的静态关系
图2:原创静态说明图。类别判断与字段提取共享同一输入错误,但承担不同的读取目的,不代表先后执行要求。

Is 和 As 可以同时匹配同一个错误。若业务需要“优先按类别处理,同时读取诊断字段”,可以独立调用两者;不要机械放在互斥分支里,然后把其中一项的信息丢掉。As 找到的是匹配对象,不是克隆副本,消费层一般应只读这些信息,避免修改共享错误对象。

包装之前先决定公开哪些底层细节

%w 不应该成为所有错误返回的自动动作。只要把底层驱动错误包装出去,调用方就可能通过 Is 或 As 观察它;日后更换驱动时,行为也可能随之改变。这是包装方案的重要取舍:上下文透明度更好,兼容责任也更大。

在本案例里,服务层公开的是自己定义的 ErrQuota 与 QuotaError,而不是数据库或网络库的私有实现。如果服务选择隐藏依赖,应在边界把底层错误映射成承诺的业务错误,并在内部另行记录诊断信息。不要简单认为换成 %v 就完成了保密:底层错误文本仍可能包含敏感内容,外部响应应使用另外设计的安全文案。

也不要为了方便提取字段,把 Token、请求原文或个人数据直接放进 Error 文本。本例只有合成的计数信息。需要程序判断的字段与要展示给用户的文字,是两个不同的设计决定。

让错误契约在后续重构中保持稳定

为可判断错误链确定三个约定即可:哪些失败必须匹配某个哨兵,哪些类型及字段允许调用方读取,哪些内部错误不会公开。添加包装层可以改变可读文本,却应保留已经承诺的类别与类型能力;主动取消包装则可能构成行为变化。

读者可以为示例补充成功、负参数、配额超限和多层包装四类测试,断言 Is/As 结果及字段,而不是把完整错误文案锁死。本例的负参数错误不属于 ErrQuota;如果未来需要独立分类,应明确新增契约,不能悄悄复用超限哨兵。

errors.Is 能代替所有 err == nil 吗?

正常成功判断继续使用 err == nil。Is 的主要用途是匹配特定错误目标,不需要把每个基础判断都改成遍历错误关系。

只有文本相同,为什么 Is 返回 false?

默认匹配不是字符串相等。不同 errors.New 返回值即使描述一致也不是同一个哨兵。应该复用声明好的 ErrQuota,而不是在判断时临时 new 一个。

自定义 Error 方法之后还必须写 Unwrap 吗?

如果这个类型只表达自身,就不必包装任何内部错误;如果它承诺保留内部原因,则需要建立对应可解包关系。本例要让 ErrQuota 可见,所以实现 Unwrap。

有关类型提取的具体要求见 errors.As 文档,包装是否构成公开承诺的讨论见 Go 官方错误设计文章,%w 的语义见 fmt.Errorf 文档。稳定的错误处理来自清晰的契约,而不是一串永远不变的报错文字。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>