应使用 errors.Is 或 errors.As 检查错误类型:errors.Is(err, io.EOF) 验证错误链中是否存在目标错误;errors.As(err, &e) 断言是否为特定自定义错误类型,避免直接比较 error 值。

如何用 testify/assert 检查错误是否为特定类型
Go 的错误是值,不是异常,所以测试时不能靠 panic 捕获,而要直接比对 error 值本身或其底层类型。常见误区是用 assert.Equal(t, err, io.EOF) —— 这在多数情况下失败,因为 io.EOF 是一个地址固定的变量,而函数返回的错误可能是新构造的、语义相同但地址不同的实例。
正确做法是用类型断言或 errors.Is(Go 1.13+):
if !errors.Is(err, io.EOF) {
t.Fatal("expected io.EOF")
}
// 或用 testify:
assert.True(t, errors.Is(err, io.EOF))
-
errors.Is判断错误链中是否存在目标错误(支持包装),适合检查标准错误如io.EOF、os.ErrNotExist - 若需验证自定义错误类型(如实现了
MyError结构体),用errors.As:var e *MyError; assert.True(t, errors.As(err, &e)) - 避免直接比较
err == someErr,除非你明确控制了错误实例的复用(极少见)
如何模拟并触发特定错误路径(如网络超时、文件不存在)
真实调用 os.Open 或 http.Get 会引入外部依赖,导致测试不稳定、慢、不可控。必须将底层依赖抽象为接口,并在测试中注入返回预设错误的 mock 实现。
例如,封装文件操作:
立即学习“go语言免费学习笔记(深入)”;
type FileReader interface {
ReadFile(name string) ([]byte, error)
}
func ProcessConfig(r FileReader, name string) error {
data, err := r.ReadFile(name)
if err != nil {
return fmt.Errorf("read config: %w", err)
}
// ...
}
- 测试时传入一个匿名实现:
ProcessConfig(&mockReader{err: os.ErrNotExist}, "config.json") - 不要在测试里写
os.Remove("config.json"); ProcessConfig(...)—— 状态难复现、并发不安全、Windows/macOS 行为不一致 - HTTP 客户端同理:把
*http.Client提取为接口或使用httptest.Server返回固定响应 + 错误状态码
如何测试错误信息字符串是否符合预期
错误消息属于用户可见输出,有时需校验其内容(比如 CLI 工具提示、API 错误详情)。但直接用 assert.Contains(t, err.Error(), "timeout") 脆弱:一旦调整措辞就失败,且无法区分是原始错误还是包装后的消息。
更稳健的方式是分层验证:
- 用
errors.Unwrap或errors.Is先确认错误类型,再对最内层错误的消息做精确匹配(如果该层消息是稳定契约) - 若必须检查包装后的完整消息,用正则或前缀判断:
assert.Regexp(t, `^read config:.*timeout$`, err.Error()) - 避免对整个
err.Error()做assert.Equal—— 包装层数变化、时间戳、路径等动态内容会导致偶然失败
为什么 t.Run 里 defer recover() 不适用于 Go 错误测试
有些开发者尝试用 defer func() { if r := recover(); r != nil { ... } }() 来“捕获错误”,这是根本性误解:error 是普通返回值,不是 panic。Go 中只有显式 panic 才触发 recover,而标准错误处理流程从不 panic(除非你自己写了 panic(err),这本身就不符合 Go 习惯)。
这种写法不仅无效,还会掩盖真实问题:
- 函数正常返回
err != nil时,recover()什么也得不到,测试逻辑跳过 - 万一真有 panic(比如空指针),它被意外捕获,反而让本该失败的测试通过
- 增加理解成本,违背 Go 错误即值的设计哲学
真正要测的永远是函数返回的 error 值本身,而不是试图把它当成异常来“抓”。










