Go 返回了 nil 指针,调用方的 err != nil 仍然成立
空指针转成 error 后仍携带动态类型。先在具体指针层判断,再显式返回 nil,避免成功结果进入错误分支。
检查函数返回空指针,上层包装为 error 后却进入错误分支。下面的示例可以看出差别:
package main
import "fmt"
type CheckError struct{ Message string }
func (e *CheckError) Error() string { return e.Message }
func check() *CheckError { return nil }
func run() error { return check() }
func main() {
err := run()
fmt.Printf("type=%T, value=%#v, nil=%v\n", err, err, err == nil)
}
预期能看到类型 *main.CheckError、值 (*main.CheckError)(nil),以及 nil=false。这适用于 Go 的接口赋值和返回值转换规则,不取决于是否使用具名返回值。
空指针进入接口后,类型仍然存在
可以把接口值理解为动态类型和动态值的组合。check() 的结果虽然是空指针,但类型是 *CheckError。返回到声明为 error 的函数时,它成为带有该动态类型的接口值。
只有接口本身没有动态类型和值时,才等于 nil。应沿返回路径找到具体指针转成接口的位置。
排查时可使用上面的 %T 和 %#v 检查受控样本。不要为了确认“是否为空”直接调用 err.Error():这个示例的方法会读取空接收者的字段并导致 panic。方法能否接受空指针取决于其实现,不能一概推断。
在转换边界显式返回 nil
如果底层接口暂时不能改变,在包装函数里先判断具体指针:
func run() error {
result := check()
if result == nil {
return nil
}
return result
}
这里的判断发生在 result 仍是 *CheckError 时。成功分支返回没有动态类型的 nil;失败分支再把非空指针转成 error,调用方继续使用普通的 err != nil 即可。
如果检查函数本来就是用来报告错误,更直接的接口是返回 error,并让成功分支返回字面量 nil:
func check(valid bool) error {
if !valid {
return &CheckError{Message: "invalid input"}
}
return nil
}
即使返回类型改成了 error,也不要再声明一个空的 *CheckError,最后统一 return 这个指针;那样仍然会产生同样的接口值。
不要把调用方绑定到一种错误指针
把外层条件改成与 (*CheckError)(nil) 比较,只能识别这一种具体类型。以后增加其他错误实现时,调用方就要跟着修改。对约定“成功返回 nil”的接口,优先修正产生错误值的位置。
同样,不应把所有带空指针的接口都自动改成空接口。某些接口实现允许空接收者且有明确行为;这里的修复范围是错误返回契约,不是对全部接口值做统一清洗。
验证时至少覆盖成功和失败两条路径:成功必须满足 err == nil,失败必须保留非空错误及原有信息。若要改变公开函数签名,先检查调用方;无法同时迁移时保留底层签名,先修正包装层。回退代码后仍需检查错误分支,避免把真实失败当作成功吞掉。


