必须用 errors.Is 而不是 == 是因为 errors.Is 会递归遍历整个错误包装链匹配底层错误(如 os.ErrNotExist),而 == 仅比较地址,包装后必失败;errors.As 用于提取链中任意一层的自定义错误类型(如 *AppError)以访问其字段,二者均依赖 Unwrap() 实现。

直接回答:用 errors.Is 判断是否包含某个底层错误,用 errors.As 提取某一层的具体错误类型——两者都自动遍历整个包装链,无需手动 Unwrap。
什么时候必须用 errors.Is 而不是 ==?
当你想判断一个被多层 %w 包装的错误是否“本质上是”某个标准错误(比如 os.ErrNotExist 或自定义的 ErrTimeout)时,== 会失败,因为外层包装后地址已变。
-
errors.Is(err, os.ErrNotExist)会递归检查每一层Unwrap()后的结果,直到匹配或返回nil - 即使错误是
fmt.Errorf("loading config: %w", os.ErrNotExist),也能正确识别 - 不要写
err == os.ErrNotExist—— 这在任何包装后都会返回false
怎么从错误链里取出你的自定义错误字段?
用 errors.As 把错误链中“某一层”的具体类型赋值给变量,从而访问其结构体字段(如 Code、RetryAfter)。
type AppError struct {
Code string
Msg string
Err error
}
func (e *AppError) Error() string { return e.Msg }
func (e *AppError) Unwrap() error { return e.Err }
// 使用
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
log.Printf("业务错误码:%s,消息:%s", appErr.Code, appErr.Msg)
}
}
-
errors.As不要求目标错误一定在最外层,只要链中任意一层是*AppError类型就成功 - 注意传入的是
&appErr(指针),否则无法赋值 - 如果只是想判断类型是否存在,不用字段,也可以用
errors.As(err, new(*AppError))
为什么不能靠字符串匹配或日志搜索来判断错误?
字符串匹配脆弱、易失效;日志里看不到原始错误类型,也无法做程序化处理(比如自动重试、降级、熔断)。
立即学习“go语言免费学习笔记(深入)”;
- 比如
err.Error()返回"failed to connect to DB: dial tcp 127.0.0.1:5432: connect: connection refused",但下个版本可能加了时间戳或换行,匹配就崩了 - 而
errors.Is(err, syscall.ECONNREFUSED)是类型安全的,且兼容所有包装层级 - 第三方库(如
pgconn)返回的*pgconn.PgError也支持errors.As直接提取,无需解析字符串
拆解错误链时最容易忽略的一点
errors.Is 和 errors.As 都只对实现了 Unwrap() error 的错误生效——如果你自己写的错误类型没实现它,包装链就断了。
- 自定义错误必须显式实现
Unwrap() error,返回被包装的Err字段 - 漏掉这行,
errors.Is就只能看到最外层,再也触不到原始错误 - 用
fmt.Errorf("xxx: %w", err)包装时,标准库自动帮你实现了Unwrap;但手写结构体时不会自动补










