结论:Go 接口值是(动态类型, 动态值)二元组,只有两者都为空接口才是 nil。把 var p *T = nil 赋给接口后,动态类型是 *T、值是 nil——接口非 nil,err != nil 判断通过,调用方法时解引用 nil 指针 panic(或方法内防御性返回)。这是 Go 最著名的接口陷阱。
展开:经典翻车代码:
func query() *User { return nil } // 出错时返回 nil
func GetUser() error {
u := query()
if u == nil { return nil }
return &UserError{u} // 包成接口后非 nil
}
// 更典型:
func do() error {
var e *MyError
if failed { e = &MyError{} }
return e // 返回类型是 error,e 为 nil 时接口仍非 nil
}
规避守则:1)函数返回接口类型时,错误路径直接 return nil,绝不返回具体类型的 nil 指针;2)必须返回具体指针时,函数签名就用具体类型,让调用方决定何时转接口;3)方法接收者为指针时养成判 nil 的习惯(if r == nil),让 nil 接收者方法成为合法的"空操作"——标准库 (*net.IP).String 就这么干。检测手段:reflect.ValueOf(i).IsNil()(需先判断 Kind 是 ptr/map/chan 等),或 1.20+ 可比性的讨论。易错点:== 比较两个接口在动态类型不可比较(如 slice 动态类型)时 panic——接口比较的前提是可比较类型。
追问方向:接口值的内存布局(itab 缓存方法表)、nil map/nil slice/nil channel 各自的读写行为差异。