应使用 errors.Is 或 errors.As 断言错误:errors.Is(err, target) 判断同类错误(支持包装链),errors.As(err, &target) 提取底层错误类型或字段;避免字符串比对、reflect.DeepEqual 或直接类型断言。

怎么断言函数返回了特定错误
Go 里错误不是异常,error 是个接口,直接用 == 比对往往失败——因为多数错误是运行时新建的结构体实例,地址不同。
正确做法是用标准库的 errors.Is 或 errors.As:
-
errors.Is(err, targetErr):判断是否为同一类错误(支持包装链,比如fmt.Errorf("wrap: %w", io.EOF)也能被识别为io.EOF) -
errors.As(err, &target):想获取底层错误的具体类型或字段时用,比如提取自定义错误里的状态码
别写 if err != nil && err.Error() == "expected" ——字符串比对脆弱,一改提示就挂;也别用 reflect.DeepEqual,它不处理错误包装,且性能差、语义错。
测试自定义错误类型时要注意什么
如果你定义了 type MyError struct{ Code int; Msg string } 并实现了 Error() 方法,测试时得小心两件事:
立即学习“go语言免费学习笔记(深入)”;
- 用
errors.As才能安全转成具体类型,而不是类型断言err.(*MyError)——后者在错误被fmt.Errorf("%w", err)包装后会失败 - 如果要比较字段值(比如
Code == 404),必须先errors.As成功,再访问字段;否则 panic 或空指针 - 别忘了给自定义错误加
Unwrap() error方法,否则errors.Is和As无法穿透包装
示例:
var e *MyError
if errors.As(err, &e) {
if e.Code != 404 {
t.Errorf("expected code 404, got %d", e.Code)
}
}
为什么 t.Fatal 不该用在错误检查分支里
常见写法:if err != nil { t.Fatal(err) } ——这会让测试提前退出,后续的断言、清理逻辑全跳过,掩盖真正问题。
更稳妥的方式是把错误检查和断言拆开:
- 用
require.NoError(t, err)(来自testify)或原生if !assert.NoError(t, err),它们只标记失败但继续执行 - 如果必须用原生测试包,写成
if err != nil { t.Errorf("unexpected error: %v", err); return },至少保留单测的可读性 - 特别注意 defer 清理:用
t.Cleanup而不是依赖函数末尾执行,因为Fatal会跳过 defer
mock 外部调用返回错误时容易漏掉的点
比如 mock 数据库查询,你让 db.QueryRow().Scan() 返回 sql.ErrNoRows,但测试没通过?可能卡在这几个地方:
- 没用
errors.Is(err, sql.ErrNoRows),而是写了err == sql.ErrNoRows——sql包内部可能重新 wrap 了一次 - mock 返回的是
fmt.Errorf("query failed: %w", sql.ErrNoRows),但你的断言没考虑包装,errors.Is就能解决 - 忘记在 mock 实现里返回非 nil 的
error,比如写了return nil, nil,结果测试跑的是“无错误路径”
真实场景中,错误常被多层包装,靠字符串或指针比对基本不可靠。只要涉及错误断言,errors.Is 和 errors.As 就是默认起点,绕不开。










