1. 引言
接口(interface
)是 Go 语言实现多态和代码解耦的核心。一个变量如果实现了接口要求的所有方法,我们就可以说它"是"这个接口类型。这种动态的类型行为背后,是一套清晰而高效的内存布局和派发机制。
本文将深入 runtime
,揭示 Go interface
的两种内部表示 eface
和 iface
,并解释方法调用(动态派发)是如何实现的。
2. interface
的两种内部表示
interface
在 Go 的底层有两种不同的结构体表示,取决于接口的类型。
a) eface
(Empty Interface)
用于表示空接口 interface{}
。任何类型都可以赋值给空接口。
Go
// src/runtime/runtime2.go
type eface struct {
_type *_type // 指向变量的动态类型信息
data unsafe.Pointer // 指向变量的实际数据
}
-
_type
: 是一个runtime._type
结构体指针,包含了关于这个变量的所有类型信息(如类型名称、大小、哈希值等)。 -
data
: 是一个指针,指向被存入接口的实际数据的副本。
当执行 var i interface{} = "hello"
时,i
在内存中就是一个 eface
,其 _type
指向 string
的类型信息,data
指向字符串 "hello" 的数据。
b) iface
(Interface with Methods)
用于表示带有方法的接口,例如 io.Reader
。
Go
// src/runtime/runtime2.go
type iface struct {
tab *itab // 接口方法表指针
data unsafe.Pointer // 指向变量的实际数据
}
-
data
: 与eface
相同,指向实际数据。 -
tab
(itab
) : 这是实现动态派发的关键。itab
(interface table) 是一个结构体,包含了:-
inter
: 指向接口类型的定义。 -
_type
: 指向具体类型(动态类型)的定义。 -
fun
: 一个函数指针数组。这个数组的长度等于接口定义的方法数量。数组中的每个指针都指向具体类型所实现的对应方法。
-
当执行 var r io.Reader = os.File{}
时,Go runtime
会在内部构建一个 itab
。这个 itab
会确认 os.File
类型确实实现了 io.Reader
的所有方法(如 Read()
),然后将 os.File
的 Read
方法的函数地址存入 itab
的 fun
数组中。最后,用这个 itab
和指向 os.File
数据的指针来填充 iface
。
3. 动态派发 (Dynamic Dispatch)
当我们通过一个接口变量调用方法时,例如 r.Read(...)
,其执行流程如下:
-
从
iface
中取出tab
(itab
) 指针。 -
从
itab
的fun
数组中,根据方法在接口定义中的顺序,找到对应的函数指针。例如,Read
是io.Reader
的第一个方法,就取fun[0]
。 -
从
iface
中取出data
指针(即receiver
)。 -
将
data
作为第一个参数(receiver
),调用获取到的函数指针。
这个通过 itab
查找并调用方法的过程,就叫做动态派发。因为具体调用哪个函数是在运行时才决定的,所以会比直接调用(静态派发)有微小的性能开销。
4. 类型断言 (value, ok := i.(T)
)
类型断言的实现也依赖于 eface
和 iface
。
-
对于
i.(T)
,runtime
会取出i
内部的_type
(来自eface
或iface.tab._type
),并将其与T
的类型信息进行比较。 -
如果类型完全匹配,断言成功,
ok
为true
,value
被赋予data
指针指向的数据。 -
如果不匹配,断言失败,
ok
为false
,value
为T
的零值。如果是不带ok
的断言,则会直接panic
。