用 `iface` 与 `eface` 布局图解释接口持有 nil 指针仍非 nil 的原因
解读
国内一线/二线公司面试中,这道题出现的频率极高,“接口判等” 是 Go 语言最经典的“坑”之一。面试官真正想考察的是:
- 候选人是否亲手调试过接口底层布局;
- 能否用内存视角把“表象”与“实现”对应起来;
- 是否具备防御式编码意识,能在业务代码里规避此类空指针误判。
答题时切忌只背结论,必须徒手把 iface/eface 的两段式结构画在纸上(即使现场无图,也要用“itab 指针 + data 指针”文字描述清楚),让面试官看到你“脑内还原内存布局”的能力。
知识点
-
接口的两种底层表示
eface(empty interface):type *rtype+data unsafe.Pointeriface(non-empty interface):itab *itab+data unsafe.Pointer
-
itab 结构体 里既包含动态类型元信息,也包含函数地址表,是 Go 实现多态的关键。
-
接口变量 == nil 的充要条件 是“type 字段为 nil 且 data 字段为 nil”,二者缺一不可。
-
“typed nil” 现象:当把某个具体类型的 nil 指针赋给接口时,仅 data 为 nil,type/itab 已指向该类型的元数据,因此接口整体不为 nil。
-
编译器生成的转换代码(convT2E、convT2I)会在运行期把具体值包装成上述两段式结构,nil 指针同样会被包装,于是出现“看起来是 nil,实则非 nil”的陷阱。
答案
我先给出内存布局文字图,再解释原因。
eface 布局(runtime 定义)
+------------------+
| *rtype type | <-- 指向类型元数据
+------------------+
| unsafe.Pointer | <-- 指向实际数据
+------------------+
iface 布局(runtime 定义)
+------------------+
| *itab tab | <-- 包含类型元数据与方法表
+------------------+
| unsafe.Pointer | <-- 指向实际数据
+------------------+
判 nil 规则:只有当 type/tab == nil 且 data == nil 时,接口变量才等于 nil。
场景演示:
var p *T = nil // p 是 typed nil
var i interface{} = p // 发生 eface 包装
此时:
eface.type已指向*T的rtype,不为 nil;eface.data虽为 0x0,但第一条已非 nil;
因此i == nil返回 false。
结论:接口持有 nil 指针仍非 nil,是因为动态类型信息已被写入 type/tab 字段,接口变量整体已具备类型,仅 data 为 nil 不足以满足“接口 nil”定义。
拓展思考
- 防御式编码:对外暴露的 API 若返回接口,必须显式返回 nil,而非返回具体类型的 nil 指针;否则调用方极易踩坑。
- reflect 陷阱:
reflect.ValueOf(nil)得到零 Value,但reflect.ValueOf((*T)(nil))得到非零 Value,原因同样是 eface 已携带类型。 - 错误链场景:
errors.Wrap(nil, "msg")在部分版本里曾返回非 nil 接口,导致if err != nil误判,本质也是 typed nil 问题。 - 性能角度:避免频繁把 nil 指针转成接口,会额外分配itab/rtype 缓存与栈内存;高频路径可返回
(nil, false)元组替代接口。