相关数据结构
1. 接口类型 - iface
type iface struct {
tab *itab // 接口类型信息,包含函数入口表
data unsafe.Pointer // 接口所引用的对象即数据
}
显然,接口的数据存储只需要2个pointer的大小。
2. 接口类型信息 - itab
type itab struct {
inter *interfacetype // 接口的(表面上的)类型
_type *_type // 接口真实的类型,即指向的对象的类型
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
接口类型信息 记录的一个对象到一个接口的映射信息。于一个确定的接口和一个确定的类型,这个信息只存在一份。
成员fun 是接口的函数入口地址的列表,在64位编译器上,fun的偏移刚好是0x18(8 + 8 + 4 + 4 = 16 )。
接口实现原理
- 接口的赋值
其实就是对 iface 结构的填装。
后面会通过源码讲述iface.tab 值的来处。 - 对接口进行函数的调用
对于一个确定的接口,其函数在结构itab的fun中的编号(index)以然确定,所以调用函数的时候很简单,直接调用地址 iface.data.fun[index]即可。
源码分析
1. 源测试代码
import "fmt"
type car interface {
run3()
run4()
run5()
}
type car2 interface {
run3()
run5()
}
type smallCar struct {
}
func (self *smallCar)run1() {
fmt.Println("s run1")
}
func (self *smallCar)run2() {
fmt.Println("s run2")
}
func (self *smallCar)run3() {
fmt.Println("s run3")
}
func (self *smallCar)run4() {
fmt.Println("s run4")
}
func (self *smallCar)run5() {
fmt.Println("s run5")
}
func main() {
var c car
var c2 car2
var sco smallCar
sco.run3()
c = &sco
c.run3()
c2 = c
c2.run3()
}
2. 汇编代码
main.go:38 0x4af359 488b8900000000 mov rcx, qword ptr [rcx]
main.go:38 0x4af360 483b6110 cmp rsp, qword ptr [rcx+0x10]
main.go:38 0x4af364 0f86c5000000 jbe 0x4af42f
=> main.go:38 0x4af36a* 4883ec68 sub rsp, 0x68
main.go:38 0x4af36e 48896c2460 mov qword ptr [rsp+0x60], rbp
main.go:38 0x4af373 488d6c2460 lea rbp, ptr [rsp+0x60]
main.go:39 0x4af378 0f57c0 xorps xmm0, xmm0
main.go:39 0x4af37b 0f11442450 movups xmmword ptr [rsp+0x50], xmm0
main.go:40 0x4af380 0f57c0 xorps xmm0, xmm0
main.go:40 0x4af383 0f11442440 movups xmmword ptr [rsp+0x40], xmm0
main.go:41 0x4af388 488d0591aa0100 lea rax, ptr [__image_base__+826912]
main.go:41 0x4af38f 48890424 mov qword ptr [rsp], rax
main.go:41 0x4af393 e898cdf5ff call $runtime.newobject
main.go:41 0x4af398 488b442408 mov rax, qword ptr [rsp+0x8] //rax就是对象sco的地址了
main.go:41 0x4af39d 4889442438 mov qword ptr [rsp+0x38], rax
main.go:43 0x4af3a2 4889442428 mov qword ptr [rsp+0x28], rax
//以下两行把sco放入rsp,以sco做参数调用smallCar.run3
main.go:43 0x4af3a7 48890424 mov qword ptr [rsp], rax
main.go:43 0x4af3ab e8c0fdffff call $main.(*smallCar).run3
main.go:45 0x4af3b0 488b442438 mov rax, qword ptr [rsp+0x38]
main.go:45 0x4af3b5 4889442430 mov qword ptr [rsp+0x30], rax
//以下片段对应代码 "c = &sco"
main.go:45 0x4af3ba 488d0d3f650400 lea rcx, ptr [__image_base__+1005824] //(__image_base__+1005824 ) 是 类型smallCar到接口car的映射信息
main.go:45 0x4af3c1 48894c2450 mov qword ptr [rsp+0x50], rcx
main.go:45 0x4af3c6 4889442458 mov qword ptr [rsp+0x58], rax
//以下片段对应代码 " c.run3()"
main.go:46 0x4af3cb 488b442450 mov rax, qword ptr [rsp+0x50]
main.go:46 0x4af3d0 8400 test byte ptr [rax], al
main.go:46 0x4af3d2 488b4018 mov rax, qword ptr [rax+0x18] //经过两次取值取得run3的函数入口地址
main.go:46 0x4af3d6 488b4c2458 mov rcx, qword ptr [rsp+0x58]
main.go:46 0x4af3db 48890c24 mov qword ptr [rsp], rcx
main.go:46 0x4af3df ffd0 call rax
//以下代码获取类型smallCar 到接口car2的映射信息 等价于伪代码
// c2 = runtime.convI2I(car2.接口类型信息, c)
main.go:48 0x4af3e1 488d0538c50100 lea rax, ptr [__image_base__+833824] //(__image_base__+833824) 为 car2的接口类型信息,对应结构为 interfacetype
main.go:48 0x4af3e8 48890424 mov qword ptr [rsp], rax
main.go:48 0x4af3ec 488b442458 mov rax, qword ptr [rsp+0x58]
main.go:48 0x4af3f1 488b4c2450 mov rcx, qword ptr [rsp+0x50]
main.go:48 0x4af3f6 48894c2408 mov qword ptr [rsp+0x8], rcx
main.go:48 0x4af3fb 4889442410 mov qword ptr [rsp+0x10], rax
main.go:48 0x4af400 e8bba3f5ff call $runtime.convI2I
main.go:48 0x4af405 488b442418 mov rax, qword ptr [rsp+0x18]
main.go:48 0x4af40a 488b4c2420 mov rcx, qword ptr [rsp+0x20]
//下面是 "c2.run3()"
main.go:48 0x4af40f 4889442440 mov qword ptr [rsp+0x40], rax
main.go:48 0x4af414 48894c2448 mov qword ptr [rsp+0x48], rcx
main.go:49 0x4af419 8400 test byte ptr [rax], al
main.go:49 0x4af41b 488b4018 mov rax, qword ptr [rax+0x18]
main.go:49 0x4af41f 48890c24 mov qword ptr [rsp], rcx
main.go:49 0x4af423 ffd0 call rax
main.go:50 0x4af425 488b6c2460 mov rbp, qword ptr [rsp+0x60]
main.go:50 0x4af42a 4883c468 add rsp, 0x68
main.go:50 0x4af42e c3 ret
main.go:38 0x4af42f e88c6afaff call $runtime.morestack_noctxt
main.go:38 0x4af434 e917ffffff jmp $main.main
3. 代码解析
汇编代码中已经对一些关键点做了注释
- 通过源码"c = &sco" 其实可以发现smallCar到car的映射信息(即 c.tab) 是硬编码的,故在在编译期就生成了(这个信息就存在于全局数据区)
- 而代码 "c2 = c" 相对麻烦一点,需要调用函数 runtime.convI2I 完成转换,可以猜测c2.tab 的获取需要费一点功夫,后面有展开说明
4. runtime.convI2I 解析
4.1 runtime.convI2I 调用过程:
runtime.convI2I -> runtime.getitab
4.2 runtime.convI2I 的作用:
- 通过c.tab 的真实类型 即c.tab._type 在hash表中runtime.itabTable查找到 smallCar 到 car2 的映射信息;
- 如果上一步找不到则通过smallCar 和 car2的类型信息生成c.tab 并存到runtime.itabTable.
通过调试发现car 向 car2的第一次转换需要生成c.tab, 因为car向 car2转换时,在编译时无法知晓car的实际类型,故尚未存在从smallCar到car2的转换,因此需要在运行期进行构造
总结与对比
- golang接口是松散型的
golang接口松散型的,意味着类型与接口之间没有显式的继承关系,是行为决定型接口为像猫就是猫,像狗就是狗。
C++等的接口的特点是血缘决定型接口,父亲是猫,即使子类再像狗也只能是猫。
- 简单背后背负了太多
a. 其实如果但只看对象向接口的转换过程,运行时其实倒没什么特别的,与C++基于vptr函数表的多态实现类似,区别在于golang对象不带有这种vptr表,接口才具有这种vptr表。
b. 但是golang接口与接口间转换就麻烦了,至少需要经过一次hash表的查询,从而得到对象对于目标接口的vptr表。而像C++这种语言,由于明确的类型继承关系,使得对于一条继承链中的任意一个类型,同一个虚函数在不同类型的vptr表中的索引是可以是一样的,因此只需要每种类型具有一个vptr表,就可以在任意一级接口调用函数时,按照
"取对象中的vptr表;通过函数索引取函数地址;对函数地址进行调用"
三部曲实现无overload的高效的运行时多态方案。
c. golang的接口的实现可以促成一个优势——能够判断接口与接口之间的转换是否合法。因为接口与接口的转换的实质是原接口指向的对象向目标接口的转换过程,而接口中携带了足够的类型信息。通过类型信息实现的反射机制,可以在运行中准确知晓对象与接口是否匹配。而C++如果不借用RTTI技术,则要求程序员在对象指针在强制转换的时候清楚的知道源对象是否是目标对象的实例,否则行为是未知的。