Go 错误处理该怎么用好 errors.Is 和 errors.As

陈默
陈默
发布时间:2026-08-25 | 72人浏览过
原创

前言

Go 开发里最难受的错误处理,往往不是写了多少个 if err != nil,而是错误一旦被层层包装,上层还能不能判断根因、拿到细节。Go 1.13 提供的 errors.Is 和 errors.As,正是为了解决这个问题:前者负责判断错误值,后者负责提取错误类型,而它们成立的前提,是先用 %w 和 Unwrap() 把错误链保留下来。

Go 的错误处理一直以“显式”著称,但在项目规模上来之后,真正麻烦的往往不是 if err != nil 本身,而是错误被多层包装后还能不能准确识别。Go 1.13 把 Unwrap()、%w、errors.Is 和 errors.As 串成了一套完整方案:既保留上下文,又不丢掉原始错误。

这篇文章就按实际排查问题的顺序来拆解:先看旧写法为什么容易失效,再看错误链是怎么建立的,最后分别说明 errors.Is 与 errors.As 该在什么场景下用。读完你可以更清楚地判断,什么时候应该比较“是不是这个错误”,什么时候应该拿到“这个错误的具体类型和附加信息”。

传统错误处理为什么不够用

在 Go 的早期版本里,错误处理方式很直接:用 error 接口表示错误,用 errors.New() 创建基础错误,用 fmt.Errorf() 补充提示信息。

用纸张卡片和箭头展示传统字符串匹配与错误链处理的差异
错误链解决了旧写法的什么问题从字符串匹配到错误链,Go 错误处理的核心变化不在于语法,而在于能否稳定保留原始错误语义。
// 传统的错误创建
err := errors.New("something went wrong")
err = fmt.Errorf("failed to process: %s", msg)

这套方式在简单程序里足够,但一旦业务链路拉长,问题就出现了。比如一次请求可能会经过 HTTP 请求、业务逻辑、数据访问、数据库操作几层调用,当数据库抛出错误时,上层通常只知道“失败了”,却很难继续判断这到底是连接超时,还是数据不存在。

很多旧代码会退回到字符串匹配:

if strings.Contains(err.Error(), "timeout") {
    // 处理超时
}

这种做法最大的麻烦有两个。第一,它非常脆弱,错误文案只要改一点,分支判断就可能失效;第二,它绕开了类型系统,让本来应该靠结构和语义判断的错误处理,变成了单纯的文本匹配。代码一多,这类判断很快就难维护。

Unwrap 和 %w 怎样建立错误链

Go 1.13 对 error 接口做了关键扩展,引入了 Unwrap() 方法:

type error interface {
    Error() string
    Unwrap() error  // Go 1.13 新增
}

有了这个方法,一个错误就可以把自己内部包装的原始错误继续暴露出来。这样一来,错误不再只是单独的一条消息,而是形成了可以逐层追溯的“错误链”。

与之配套的,就是 fmt.Errorf() 里的 %w:

// 错误包装
err := database.Query("SELECT...")
return fmt.Errorf("query failed: %w", err)

这里的重点不只是拼接出了一句更完整的提示,而是 %w 会把原始错误包装进去,让返回的新错误具备 Unwrap() 能力。这样你在上层既能看到 query failed 这样的上下文,也不会丢掉底层真正的错误对象。

这一步很重要,因为后面的 errors.Is 和 errors.As 都依赖这条错误链工作。没有链,调用方看到的就只是最终那层消息;有了链,调用方才能顺着包装关系继续往下判断和提取。

errors.Is 适合判断“是不是这个错误”

errors.Is 用来在错误链里查找某个目标错误,它的签名很简单:

func errors.Is(err, target error) bool

它会沿着整个错误链向下遍历,只要链上的某一层与 target 相等,就会返回 true。这正好解决了“错误被包装后还能不能识别”的问题。

// 定义一个 sentinel error(哨兵错误)
var ErrNotFound = errors.New("resource not found")

func findUser(id int) error {
    // 底层返回 ErrNotFound
    err := db.Find(id)
    if err != nil {
        return fmt.Errorf("find user failed: %w", err)
    }
    return nil
}

func main() {
    err := findUser(123)
    // 即使错误被层层包装,依然能正确识别
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在")
    }
}

这类场景在业务代码里非常常见:你并不关心底层错误对象里还有多少字段,只关心它是不是某个约定好的错误值,比如 ErrNotFound、权限不足、超时、重复写入等。此时用 errors.Is 最合适,因为它表达的就是“语义上的同一个错误”。

另外,errors.Is 还支持自定义匹配逻辑。如果一个错误类型实现了自己的 Is() 方法,就可以自己定义“什么叫相等”。

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Is(target error) bool {
    if v, ok := target.(*ValidationError); ok {
        return e.Field == v.Field
    }
    return false
}

上面这个例子里,哪怕 Message 不同,只要 Field 相同,就能被认为是同一类验证错误。换句话说,errors.Is 不只是做字面相等比较,它允许你把业务语义编码进错误匹配规则里。

errors.As 适合提取具体错误类型

如果说 errors.Is 解决的是“要不要进入这个分支”,那 errors.As 解决的就是“进入之后我还想拿到这个错误的详细信息”。它的签名如下:

func errors.As(err error, target interface{}) bool

它会在错误链中查找可以赋值给目标类型的错误,并把具体实例写入 target。这意味着你不仅知道“它是某种错误”,还可以进一步读取错误对象上的字段。

// 定义带额外信息的错误类型
type ParseError struct {
    File string
    Line int
    Col  int
}

func (e *ParseError) Error() string {
    return fmt.Sprintf("%s:%d:%d: parse error", e.File, e.Line, e.Col)
}

func parseConfig() error {
    err := parseFile("config.json")
    if err != nil {
        return fmt.Errorf("parse config failed: %w", err)
    }
    return nil
}

func main() {
    err := parseConfig()

    var parseErr *ParseError
    if errors.As(err, &parseErr) {
        fmt.Printf("解析错误位于 %s 的第 %d 行n", 
            parseErr.File, parseErr.Line)
    }
}

这种写法更适合结构化错误。比如解析配置文件时,你不仅想知道“解析失败了”,还希望知道具体是哪个文件、第几行、第几列出错;又比如网络、序列化、权限校验等场景,错误对象里可能会带有更多字段,供上层决定日志内容、重试策略或用户提示。

和 errors.Is 一样,errors.As 也支持自定义行为。如果错误类型实现了 As() 方法,就能自行控制如何向目标类型映射。多数日常业务代码用不到这一步,但它说明 Go 1.13 这套机制并不只是“能用”,而是给复杂错误体系留出了扩展空间。

实际使用时怎么选

把两者放在一起看,区分其实很清楚。

用两组专栏卡片对比 errors.Is 和 errors.As 的适用场景
errors.Is 和 errors.As选用标准很简单:只判断是不是某个错误,用 errors.Is;需要拿到具体类型和字段,用。
  • errors.Is:判断某个错误链里是否存在指定错误值,适合哨兵错误、语义判断和分支处理。
  • errors.As:提取某种具体错误类型,适合需要读取字段、拿到上下文信息、做细粒度处理的场景。

这套设计的价值,不只是 API 看起来更统一,而是它让“补充上下文”和“保留原始错误”不再冲突。以前你往往只能二选一:要么返回底层错误,信息太少;要么重新包装成一句新文案,结果上层再也认不出原始错误。现在通过 %w 建立错误链,再配合 errors.Is 和 errors.As,这两个目标可以同时实现。

从 errors.New、fmt.Errorf 到 Go 1.13 的这套错误链机制,Go 的错误处理并没有变复杂,反而是把原本散乱、脆弱的实践收束成了更稳定的范式。以后再遇到层层嵌套的错误,先别急着对着 err.Error() 做字符串匹配,先看它是不是应该被包装成链,再决定用 errors.Is 还是 errors.As 去处理。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表