Go 错误处理速查
errors.Is / As / Wrap / pkg/errors · 点击复制
errors.Is / As / Wrap / pkg/errors · 点击复制
调试 Go 代码时,errors.Is 和 errors.As 的签名总是记混,尤其是 pkg/errors 的 Wrap 与标准库的 %w 行为差异。这个工具列出了 errors 包和 pkg/errors 最常用的 12 个函数签名、返回类型与典型用法,每个条目附带一段可运行的示例代码片段。纯浏览器渲染,代码不离开本地,适合写单元测试前快速确认 API 行为。
后端工程师老赵在重构用户模块时,发现 log 里同时出现 'not found' 和 'permission denied' 两种 error。他需要确认业务层抛出的 'not found' 是否源于底层数据库的 'sql.ErrNoRows',而非其他包误包装的同类字符串。用 errors.Is 逐层拆解,一眼看出错误链的根因,不用再逐行打 log 或手动追源码。
微服务 A 调用 B 的 gRPC 接口返回 'context deadline exceeded',但 B 侧日志显示处理耗时仅 50ms。怀疑是 A 的客户端超时设置过短,但 B 内部又用 fmt.Errorf 包装了多层信息。把错误对象丢进本工具,errors.As 能提取出底层的 'DeadlineExceeded' 类型,直接定位到是 gRPC 拦截器层抛出的,而非业务逻辑。
用 'github.com/go-sql-driver/mysql' 时,驱动返回的 'mysql.MySQLError' 包含 Number 字段(如 1062 表示唯一键冲突)。业务代码需要根据 Number 决定重试还是报错。用 errors.As 把 error 断言成 *mysql.MySQLError,再读 Number 字段。本工具能快速验证该断言是否匹配,避免运行时 panic。
线上偶发 'nil pointer' panic,但标准 error 只显示 'runtime error: invalid memory address'。如果代码里用了 'github.com/pkg/errors' 的 errors.Wrap,本工具能解析出被包装的原始 error 及其调用栈(file:line 信息),帮测试同学小陈从 2000 行代码的 release 分支里,锁定具体是哪次 commit 引入的 nil 判断遗漏。
团队自定义了 'ErrValidationFailed' 类型,实现了 Error() 方法,但忘了实现 Is() 方法导致 errors.Is 无法匹配。把自定义类型和标准 error 输入本工具,模拟 errors.Is 的匹配逻辑,能直接发现 Is() 缺失,而不需要写单元测试跑一遍。
| 输入 | 输出 | 说明 |
|---|---|---|
| errors.Is(err, io.EOF) | true | 常规:判断错误链中是否包含特定哨兵错误,errors.Is 会沿 Wrap 链向上查找 |
| errors.As(err, &target) | true, target = *os.PathError | 常规:将错误链中匹配类型的错误赋值给 target,用于获取具体错误信息 |
| fmt.Errorf("wrap: %w", io.EOF) | wrap: EOF | 常规:使用 %w 格式化动词创建带 Wrap 的错误,errors.Is 可匹配到 io.EOF |
| errors.Is(nil, io.EOF) | false | 边界:nil 错误不匹配任何错误,避免误判 |
| errors.As(nil, &target) | false | 边界:nil 错误无法赋值,target 保持不变 |
| errors.Unwrap(fmt.Errorf("wrap: %w", io.EOF)) | EOF | 边界:Unwrap 只剥一层,返回被 Wrap 的原始错误 |
| errors.Is(err, errors.New("EOF")) | false | 易错:errors.New 每次创建新实例,即使内容相同也不相等;应使用 io.EOF 等哨兵 |
| errors.As(err, &target) 但 target 类型为 *os.PathError 而 err 是 *net.OpError | false | 易错:类型不匹配时 As 返回 false,不会 panic,但需检查返回值 |
1.errors.Is 直接比较包装后的哨兵错误
errors.Is(err, io.EOF)errors.Is(err, io.EOF) // 正确,因为 Is 会解包装errors.Is 会自动遍历错误链,不需要手动解包装。直接写即可,它内部调用 Unwrap 逐层比较。
2.用 == 比较包装后的错误
if err == io.EOF { ... }if errors.Is(err, io.EOF) { ... }== 只比较指针地址,包装后的错误是新对象。必须用 errors.Is 遍历链式比较。
3.errors.As 第二个参数传 nil 或非指针
var target *os.PathError; errors.As(err, target)var target *os.PathError; errors.As(err, &target)errors.As 第二个参数必须是**指向目标类型指针的指针**,否则无法赋值。传 nil 或直接传 target 会 panic。
4.用 errors.As 匹配接口类型时传接口值
var target error; errors.As(err, &target)var target *MyError; errors.As(err, &target)errors.As 要求目标类型是具体类型指针,不能传 error 接口本身。接口无法被赋值,As 会返回 false。
5.fmt.Errorf 用 %w 包装多个错误
fmt.Errorf("failed: %w, %w", err1, err2)fmt.Errorf("failed: %w", err1) // 或使用 errors.Joinfmt.Errorf 只支持一个 %w 占位符。多个 %w 会导致编译错误。Go 1.20 起可用 errors.Join。
6.pkg/errors.Wrap 返回 nil 时继续使用
if err != nil { return errors.Wrap(err, "msg") } else { return errors.Wrap(nil, "msg") }if err != nil { return errors.Wrap(err, "msg") } else { return nil }pkg/errors.Wrap 传入 nil 会返回 nil,但 Wrap 本身有开销。更清晰的做法是只在 err != nil 时包装。
7.自定义错误类型未实现 Unwrap 方法
type MyErr struct { Msg string; Err error }func (e *MyErr) Unwrap() error { return e.Err }要让 errors.Is/As 遍历自定义错误链,类型必须实现 Unwrap() error 方法。否则包装链断裂。
8.errors.Is 与 errors.As 混用场景
errors.Is(err, &MyError{})var target *MyError; errors.As(err, &target)errors.Is 比较值相等,适合哨兵错误;errors.As 匹配类型,适合提取具体错误实例。传值给 Is 不会匹配类型。
errors.Is(err, target) ↔ err == target ∥ errors.Is(err.Unwrap(), target)
err待检查的错误值target目标错误哨兵值err.Unwrap()err 的底层包装错误err = fmt.Errorf("连接超时: %w", io.EOF),target = io.EOF。errors.Is(err, io.EOF) 先比较 err == io.EOF(false),再递归比较 err.Unwrap() == io.EOF(true),最终返回 true。
核心区别是匹配目标类型不同。errors.Is 用来判断错误链里有没有“某个特定的值”(比如某个哨兵错误),它做的是值比较,通常用于检查 io.EOF 这种固定错误。errors.As 用来判断错误链里有没有“某种类型的错误”,它会把匹配到的错误赋值给目标变量,适合提取自定义错误里的字段。本速查表里两个函数的关键字高亮和参数类型标注就是帮你一眼区分:Is 第二个参数是 error 值,As 第二个参数是目标指针。
这大概率是用了 golang.org/x/xerrors 的 Wrap 而不是 github.com/pkg/errors 或 Go 1.20 的 fmt.Errorf("%w")。xerrors.Wrap 创建的包装错误在 Go 1.13 标准库的 errors.Is 下不兼容,因为它的 Unwrap 方法签名不同。本速查表在“Wrap 实现对比”栏明确列出了标准库 fmt.Errorf、pkg/errors 和 xerrors 三种实现的行文差异。建议迁移到 Go 1.20+ 并用 fmt.Errorf("%w: %v", err, msg) 包装,保证 Is/As 链式匹配正确。
pkg/errors 确实在 2021 年归档了,但大量遗留项目(尤其是 Go 1.12 之前启动的老项目)仍然重度依赖它。本速查表保留 pkg/errors 一栏,是因为很多开发者接手旧代码时,面对 errors.Wrap 和 errors.Cause 不知道怎么跟标准库的 errors.Is 互转。表中给出了两种代码的“等价写法对照”,比如 pkg/errors.Cause(err) 在标准库下可以用一个循环 errors.Unwrap 替代。不列它,接手旧项目的人反而更懵。
关键是要让你的自定义类型实现 error 接口(即有一个 Error() string 方法),并且最好也实现 Unwrap() error 方法,这样它才能正确参与到错误链中。如果想让 errors.As 匹配到指针类型,注意传进去的目标必须是该类型的双指针(**MyError)。本速查表中“自定义错误实现示例”给出了带字段的 MyError 完整代码,包括 Error()、Unwrap() 和 As 匹配的写法,直接复制改字段名就能用。
标准库没有提供类似 pkg/errors.Cause 的直接函数,但可以自己写一个循环:for { if err = errors.Unwrap(err); err == nil { break } }。注意这样会一路解到最底层,如果中间某个错误没有实现 Unwrap 就停在那层。更安全的做法是用 errors.Is 检查特定哨兵错误,或者用 errors.As 提取中间层的自定义错误。本速查表在“解包技巧”部分列出了三种遍历策略的代码片段,分别对应“取根错误”“取第一个可匹配类型”“取所有层级”。
本工具完全在浏览器本地运行,没有任何后端服务器。所有 errors 函数签名对比、代码示例高亮、格式化逻辑都通过前端 JavaScript 和 WASM 模块在用户浏览器中执行。输入框里贴的代码或搜索的关键词不会离开当前页面,刷新即消失。可以放心在离线环境或内网开发机上使用。
有几个常见坑。一是 errors.Is(nil, nil) 返回 false,因为 nil 不是 error。二是用 errors.As 时目标变量必须是非 nil 指针,否则 panic。三是 fmt.Errorf 如果同时用 %w 和 %v 包装多个错误,只有最后一个 %w 生效,前面的 %w 会被当成普通格式化输出。本速查表在“常见陷阱”区域集中列出了 5 条这类边界行为,每条都附了触发代码和输出结果,比官方文档的 FAQ 更贴近实际踩坑场景。
隐私保证所有计算与处理均在你的浏览器本地完成,输入数据不会上传服务器,也不会保存或共享。