Go 中接口 nil 判断的陷阱:为什么 T(nil) 不等于 nil

在 go 中,将 nil 指针赋值给接口变量时,接口并非 nil——因其内部存储了具体类型(如 *t)和 nil 值,构成 (*t, nil) 元组,而真正的 nil 接口是 (nil, nil)。这导致 if err == nil 判断失败,是 go 类型系统的核心特性,非 bug。

Go 的接口底层由两个字段组成:动态类型(dynamic type)动态值(dynamic value)。只有当二者同时为 nil 时,接口值才被视为 nil。例如:

  • var err error = nil → 底层为 (nil, nil) → err == nil 为 true;
  • var g *Goof = nil; var err error = g → 底层为 (*Goof, nil) → err == nil 为 false,尽管 g 本身是 nil。

这是 Go 语言规范明确规定的语义,而非运行时异常或设计缺陷。以下代码直观展示了该行为:

package main

import "fmt"

type Goof struct{}

func (g *Goof) Error() string { return "I'm a goof" }

func TestError(err error) {
    if err == nil {
        fmt.Println("Error is nil")
    } else {
        fmt.Println("Error is not nil") // 实际输出此行
    }
}

func main() {
    var g *Goof // g == nil
    TestError(g) // 传入的是 (*Goof, nil),非 nil 接口!
}

✅ 正确做法:始终通过 error 类型

声明或返回 nil

  • ✅ 推荐:var err error(零值即 error(nil))
  • ✅ 推荐:函数返回 error 时直接 return nil(编译器自动转换为 (nil, nil))
  • ❌ 避免:先构造 *T(nil) 再隐式转成接口,如 TestError((*Goof)(nil))

补充说明:该机制不仅影响 nil,也适用于类型差异。例如:

type Bob int
var x int = 3
var y Bob = 3
var ix, iy interface{} = x, y
fmt.Println(ix == iy) // false —— 因 (int, 3) ≠ (Bob, 3)

总结:Go 中接口的 == nil 判断是类型安全的严格比较,不是“值空”判断。编写错误处理逻辑时,应始终以 error 类型作为契约起点,避免绕过接口抽象直接操作底层结构体指针。这是理解 Go 接口本质的关键一课。