Go interface实现

463 阅读6分钟

相关数据结构

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 )。

接口实现原理

  1. 接口的赋值
    其实就是对 iface 结构的填装。
    后面会通过源码讲述iface.tab 值的来处。
  2. 对接口进行函数的调用
    对于一个确定的接口,其函数在结构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. 代码解析

汇编代码中已经对一些关键点做了注释

  1. 通过源码"c = &sco" 其实可以发现smallCar到car的映射信息(即 c.tab) 是硬编码的,故在在编译期就生成了(这个信息就存在于全局数据区)
  2. 而代码 "c2 = c" 相对麻烦一点,需要调用函数 runtime.convI2I 完成转换,可以猜测c2.tab 的获取需要费一点功夫,后面有展开说明

4. runtime.convI2I 解析

4.1 runtime.convI2I 调用过程:

runtime.convI2I -> runtime.getitab

4.2 runtime.convI2I 的作用:

  1. 通过c.tab 的真实类型 即c.tab._type 在hash表中runtime.itabTable查找到 smallCar 到 car2 的映射信息;
  2. 如果上一步找不到则通过smallCar 和 car2的类型信息生成c.tab 并存到runtime.itabTable.

通过调试发现car 向 car2的第一次转换需要生成c.tab, 因为car向 car2转换时,在编译时无法知晓car的实际类型,故尚未存在从smallCar到car2的转换,因此需要在运行期进行构造

总结与对比

  1. golang接口是松散型的

    golang接口松散型的,意味着类型与接口之间没有显式的继承关系,是行为决定型接口为像猫就是猫,像狗就是狗。
    C++等的接口的特点是血缘决定型接口,父亲是猫,即使子类再像狗也只能是猫。

  1. 简单背后背负了太多

    a. 其实如果但只看对象向接口的转换过程,运行时其实倒没什么特别的,与C++基于vptr函数表的多态实现类似,区别在于golang对象不带有这种vptr表,接口才具有这种vptr表。
    b. 但是golang接口与接口间转换就麻烦了,至少需要经过一次hash表的查询,从而得到对象对于目标接口的vptr表。而像C++这种语言,由于明确的类型继承关系,使得对于一条继承链中的任意一个类型,同一个虚函数在不同类型的vptr表中的索引是可以是一样的,因此只需要每种类型具有一个vptr表,就可以在任意一级接口调用函数时,按照
"取对象中的vptr表;通过函数索引取函数地址;对函数地址进行调用"
三部曲实现无overload的高效的运行时多态方案。
    c. golang的接口的实现可以促成一个优势——能够判断接口与接口之间的转换是否合法。因为接口与接口的转换的实质是原接口指向的对象向目标接口的转换过程,而接口中携带了足够的类型信息。通过类型信息实现的反射机制,可以在运行中准确知晓对象与接口是否匹配。而C++如果不借用RTTI技术,则要求程序员在对象指针在强制转换的时候清楚的知道源对象是否是目标对象的实例,否则行为是未知的。