iface和eface本质区别在于是否需要动态方法派发:iface含tab*itab和data,用于非空接口,支持方法调用;eface仅含_type和data,用于interface{},不存方法、不支持调用,所有操作基于_type校验。

面试时说清 interface 底层,关键不是背结构体字段,而是讲明白:谁在什么时候存了什么、为什么不能省、错在哪一步会挂。
iface 和 eface 的区别不能只说“有没有方法”
iface 是非空接口(比如 io.Reader)的运行时表示,含两个字段:tab *itab 和 data unsafe.Pointer;eface 是 interface{} 的表示,只有 _type 和 data。区别不在“有没有方法声明”,而在“是否需要方法跳转”——interface{} 根本不支持调方法,连语法都不让写 v.Read(),编译直接报错,不是 panic。
常见误答是:“空接口没 itab 是为了节省内存”。错。真正原因是:它压根不需要动态派发,所有操作(断言、反射)都基于 _type 查表,和方法无关。
- 传
*os.File给io.Reader→ 构建iface,填tab(含Read地址)和data - 传
*os.File给interface{}→ 构建eface,只填_type(指向*os.File元信息)和data - 两者
data字段行为一致:指针类型传进去,data就等于那个指针值;值类型传进去,data指向副本
itab 不是编译期静态表,是运行时懒加载的二元映射
itab 本质是「接口类型 + 具体类型」的缓存键值对,首次赋值才生成,之后复用。它不共享:哪怕 A 和 B 两个接口方法签名一模一样,*T 赋给 A 和赋给 B 会生成两个独立 itab。
构建失败不是运行时 panic,而是编译失败。典型错误:
-
func (t T) M()(值接收者),却把&t赋给接口 → 可以,&t的方法集包含T.M -
func (t *T) M()(指针接收者),却把t(值)赋给接口 → 编译报错:T does not implement X (M method has pointer receiver)
注意:这个检查发生在赋值语句编译期,不是你调 x.M() 的时候。面试时如果说“调用时才检查”,就露馅了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
方法调用实际走的是 itab.fun 数组间接跳转
当你写 r.Read(buf)(r 是 io.Reader),底层三步:
- 从
r的iface.tab取出指针 - 查
tab.fun[0](Read是io.Reader第一个方法)拿到函数地址 - 把
r.data当作 receiver,跳过去执行
这个过程无法内联,热点路径中 runtime.ifaceE2I 或 runtime.interfacelookup 在 pprof 里占比升高,就是它在拖慢性能。别信“Go 接口调用很快”的笼统说法——快是相对反射,不是相对直接调用。
另外,itab.fun 里存的是函数地址,不是方法体;receiver 重绑定由调用约定完成,不是靠复制对象。
interface{} 类型断言失败的 panic 真实来源
v.(string) panic 报 “interface conversion: interface {} is nil, not string”,不是因为 v 是 nil,而是因为 v 的 eface 中 data == nil 但 _type != nil(比如你写了 var v interface{} = (*string)(nil))。这时 v 本身非 nil(_type 有值),但解包失败。
真正的全零值 interface{}(_type == nil && data == nil)做断言会直接返回 false,不 panic。容易被忽略的是:map 查不到 key 时返回的零值 interface{},其 _type 是 nil,但如果你先做了 var x interface{} = m["k"] 再断言,行为取决于 m["k"] 实际返回什么——可能为 nil,也可能为 nil 指针,这俩在 eface 里表现完全不同。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










