Go中error接口变量不为nil≠底层值为nil,因接口包含类型信息;应使用errors.Is/As而非==nil判断,recover需先类型断言再处理。

nil error 不等于 nil 接口值
Go 里 error 是接口类型,声明为 error 的变量即使底层值是 nil,只要它被赋给了一个非 nil 的接口变量,整个接口值就不为 nil。这是最常踩的坑:你写了 if err != nil,结果还是 panic 或逻辑跳过。
- 常见错误现象:
err看似没出错,但if err != nil没进,后续调用err.Error()却 panic —— 因为err是一个 *nil 指针实现的 error 接口*,接口本身不为 nil,但底层 concrete value 是 nil - 典型场景:自定义 error 类型(比如
type MyErr struct{ msg string })忘了实现Error() string方法,或返回了&MyErr{}但该结构体字段未初始化,导致方法内解引用 panic - 实操建议:永远用
errors.Is(err, xxx)或errors.As(err, &target)判断语义错误;避免仅靠== nil做控制流分支 - 性能影响:
errors.Is和errors.As有少量反射开销,但比运行时 panic 强得多;日常 HTTP handler、DB 查询等场景完全可接受
为什么 *MyError == nil 会误判
当你把一个 *MyError 类型的指针赋给 error 接口,哪怕这个指针是 nil,接口值仍包含类型信息,因此不等于 nil。但如果你直接拿 *MyError 变量和 nil 比,结果又是 true —— 类型不同,行为就分裂了。
- 常见错误现象:
var e *MyError; if e == nil { ... }成立,但err := error(e); if err == nil { ... }不成立,导致日志漏打或 fallback 逻辑失效 - 参数差异:接口比较看的是
(type, value)二元组;而指针比较只看地址是否为空;混用二者等于在类型系统边缘走钢丝 - 实操建议:所有 error 流程统一走
error类型变量,不要在中间穿插具体指针类型判断;如果必须检查底层指针,用errors.As(err, &e)再判e == nil
recover 里捕获到的 error 为什么总是 nil
recover() 返回的是 interface{},不是 error。直接断言 err := recover().(error) 在 panic 值不是 error 类型时会 panic 二次;更隐蔽的是,如果 panic 的是 nil,断言会失败并返回零值 —— 你以为拿到了 error,其实拿到的是 nil 的 error 接口,又掉进上一个坑。
- 常见错误现象:写了个 defer + recover,log 打印
err:,但实际 panic 是字符串或结构体,根本没被捕获成功 - 实操建议:先做类型安全断言:
if r := recover(); r != nil { if err, ok := r.(error); ok { /* 处理 */ } else { /* 记录 r 的真实类型,比如 fmt.Sprintf("%v", r) */ } } - 兼容性注意:Go 1.22+ 对
recover()返回值做了更严格的类型约束,但老代码仍需手动兜底
测试中构造“假 nil error”的典型方式
单元测试时想模拟一个“看起来像 nil 但其实是 error 接口”的值,不能简单写 var err error(那是真 nil),也不能写 err := (*MyErr)(nil)(赋给 interface 后不为 nil)。得用类型擦除再还原的方式。
立即学习“go语言免费学习笔记(深入)”;
- 常见错误现象:测试里写
err := (*MyErr)(nil); assert.Nil(t, err)通过,但一放进函数参数(func foo(err error))就变成非 nil,测试失真 - 实操建议:用
var err error = nil构造真 nil;若要测接口非 nil 但行为像 nil,定义一个空实现:var err error = (*alwaysNilErr)(nil),其中type alwaysNilErr struct{}实现Error() string { return "" } - 容易被忽略的地方:mock 框架(如 gomock)生成的 error 返回值默认是具体类型指针,不是真 nil —— 查日志时看到
&{}就该警觉










