文章目录

Go 错误链:%w 动词与 errors.Is()/As()

发布于 2026-04-02 11:06:15 · 浏览 9 次 · 评论 0 条

Go 错误链:%w 动词与 errors.Is()/As()

Go 语言从 1.13 版本开始引入了错误链(error wrapping)机制,允许你在返回错误时“包装”原始错误,同时保留其身份信息。这一机制的核心是 %w 动词、errors.Is()errors.As() 函数。掌握它们能让你写出更健壮、可调试的错误处理逻辑。


为什么需要错误链?

在 Go 中,函数通常通过返回 error 值表示失败。传统做法是直接返回新错误(如 fmt.Errorf("xxx")),但这会丢失原始错误的信息。例如:

func readFile() error {
    _, err := os.Open("missing.txt")
    if err != nil {
        return fmt.Errorf("failed to read file")
    }
    return nil
}

调用方收到 "failed to read file" 后,无法知道底层是否是 os.ErrNotExist(文件不存在)。这导致无法做精确的错误判断。

使用 %w 包装错误可以解决这个问题:它将原始错误嵌入新错误中,形成一条“错误链”。


步骤一:用 %w 包装错误

创建一个包装错误时,在 fmt.Errorf 的格式字符串中使用 %w 动词,并将原始错误作为参数传入。

func readFile() error {
    _, err := os.Open("missing.txt")
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }
    return nil
}

关键点:

  • %w 必须是小写。
  • 只能出现一次。
  • 参数必须是非 nil 的 error

此时,返回的错误既包含你添加的上下文("failed to read file"),又保留了原始的 *fs.PathError(其中包含 os.ErrNotExist)。


步骤二:用 errors.Is() 判断错误是否匹配

调用 errors.Is(err, target) 来检查错误链中是否存在与 target 相等的错误。

err := readFile()
if errors.Is(err, os.ErrNotExist) {
    // 处理“文件不存在”的情况
}

errors.Is() 会沿着错误链逐层检查,只要某一层的错误等于 target(通过 ==errors.Is 方法),就返回 true

常见用法:

  • 检查是否为 io.EOF
  • 检查是否为 os.ErrPermission
  • 检查是否为自定义的 sentinel error(哨兵错误)

步骤三:用 errors.As() 提取特定类型的错误

有时你需要访问原始错误的字段或方法(比如获取文件路径)。这时使用 errors.As(err, &target) 将错误链中第一个匹配类型的错误赋值给 target

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Failed on path:", pathErr.Path)
}

注意:

  • 第二个参数必须是指向指针的指针(**T)。
  • errors.As() 会遍历错误链,找到第一个可转换为 *T 类型的错误。
  • 如果找到,就把该错误赋值给 *target 并返回 true

自定义错误类型支持错误链

如果你定义了自己的错误类型,想让它参与错误链,只需实现 Unwrap() error 方法。

type MyError struct {
    msg string
    inner error
}

func (e *MyError) Error() string { return e.msg }
func (e *MyError) Unwrap() error { return e.inner }

这样,errors.Is()errors.As() 就能自动穿透你的错误类型。

你也可以实现 Is(target error) bool 方法来自定义相等逻辑:

func (e *MyError) Is(target error) bool {
    return target == ErrCustom
}

errors.Is(err, ErrCustom) 被调用时,会优先调用此方法。


常见错误与注意事项

  1. 不要滥用 %w
    只有当你希望调用方能检查原始错误时才使用 %w。如果只是记录日志,用 %v 即可。

  2. 避免重复包装
    不要多次包装同一个错误,否则会增加链的深度,影响性能和可读性。

  3. %w%v 的区别

    • %w:保留错误身份,支持 Is/As
    • %v:仅格式化为字符串,丢失原始错误
  4. 不要对 nil 错误使用 %w
    fmt.Errorf("msg: %w", nil) 会 panic。

  5. errors.As() 的目标必须是指针变量地址
    错误写法:errors.As(err, &os.PathError{})(不能取字面量地址)
    正确写法:先声明变量 var p *os.PathError,再传 &p


实战示例:完整的错误处理流程

package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    if err := processFile("config.txt"); err != nil {
        // 检查是否文件不存在
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Config file missing, using defaults.")
            return
        }

        // 尝试提取路径信息
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Access denied to %s\n", pathErr.Path)
            return
        }

        // 兜底处理
        fmt.Printf("Unexpected error: %v\n", err)
    }
}

func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("cannot open config: %w", err)
    }
    defer f.Close()

    // ... 处理文件
    return nil
}

运行此程序:

  • config.txt 不存在,输出 "Config file missing, using defaults."
  • 若存在但无权限,输出 "Access denied to config.txt"
  • 其他错误则打印完整错误信息

错误链行为对比表

操作 使用 %v 使用 %w
fmt.Errorf("msg: %v", err) 仅拼接字符串,丢失原始错误 ❌ 不适用
fmt.Errorf("msg: %w", err) ❌ 不适用 保留原始错误,支持 Is/As
errors.Is(err, target) 仅比较最外层错误 遍历整个错误链
errors.As(err, &v) 仅尝试转换最外层错误 遍历链直到找到匹配类型

总结关键规则

  • 包装错误时,用 fmt.Errorf("context: %w", originalErr)
  • 判断错误类型时,用 errors.Is(err, specificError)
  • 提取错误详情时,用 errors.As(err, &targetPointer)
  • 自定义错误,实现 Unwrap() 方法以支持链式操作

遵循这些规则,你的 Go 程序将具备清晰、可追溯、可分类的错误处理能力。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文