Go语言--接口的内部实现

1 接口内部实现

毫无疑问,接口是Go语言类型系统的灵魂,是Go语言实现多态和反射的基础。前面我们结束了接口的基本概念和用法,定义接口只需要简单声明一个方法集合即可,定义新类型时不需要显式地声明要实现的接口,接口的使用也很简单。但这一切语言特性的背后是语言设计者们的智慧:把复杂留给自己,把简单留给用户。

接口的底层是如何实现的?如果实现动态调用?接口的动态调用到底需要多大的额外开销?这是我们需要弄清楚的问题。

<备注> 基于 go1.15版本。

1.1 数据结构

接口变量必须初始化才有意义,没有初始化的接口变量的默认值是nil。具体类型实例传递给接口称为接口的初始化。接口的初始化过程中,编译器通过特定的数据结构描述这个过程。非空接口的内部数据结构如下:

// src/runtime/runtime2.go
type iface struct {
    tab  *itab            //itab存放类型及方法指针信息
    data unsafe.Pointer   //数据信息
}

非空接口的初始化过程就是初始化一个 iface 类型的结构体。

  • itab:是一个结构体,用来存放接口自身类型和并绑定的实例类型及实例相关的函数指针,具体内容下面会详细介绍。
  • 数据指针data:指向接口绑定的实例的副本,接口的初始化也是一种值拷贝。data指向具体的实例数据,如果传递给接口的是值类型,则data指向的是实例的副本,如果传递给接口的是指针类型,则data指向指针副本。总而言之,无论是接口的转换,还是函数的调用,Go遵循一样的规则——值传递。

itab 结构体

// src/runtime/runtime2.go
type itab struct {
    inter *interfacetype  //接口自身的静态类型
    _type *_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.
}

itab结构体有5个字段:

  • inner:指向接口类型元信息的指针。
  • _type:是指向接口存放的具体类型元信息的指针,iface里的data指针指向的是该类型的值。一个是类型信息,另一个是类型的值。
  • hash:具体类型的哈希值。_type 里面也有hash,这里冗余存放主要是为了接口类型断言或类型查询时快速访问。
  • fun:是一个函数指针,可以理解为C++对象模型里面的虚拟函数指针,这里虽然只有一个元素,实际上指针数组的大小是可变的,编译器负责填充,运行时使用底层指针进行访问,不会受struct类型越界检查的约束,这些指针指向的是具体类型的方法。

itab 这个数据结构是非空接口实现动态调用的基础,itab 的信息被编译器和链接器保存了下来,存放在可执行文件的只读存储端(.rodata)中。itab 存放在静态分配的存储空间中,不受GC(垃圾回收器)的限制,其内存不会被回收。

_type 结构体

Go语言是一种强类型语言,编译器在编译时会做严格的类型校验。所以Go语言必然为每种类型维护一个类型的元信息,这个元信息在运行和反射时都会用到,Go语言的类型元信息的通用结构是 _type,其他类型都是以 _type 为内嵌字段封装而成的结构体。

// src/runtime/type.go
type _type struct {
    size       uintptr  //大小
    ptrdata    uintptr  // size of memory prefix holding all pointers
    hash       uint32   //类型哈希值
    tflag      tflag    //类型的特征标记
    align      uint8    //_type作为整体变量存放时的字节对齐数
    fieldAlign uint8    //当前结构体字段的对齐字节数
    kind       uint8    //基础类型枚举值和反射中的Kind一致,kind决定了如何解析该类型
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(unsafe.Pointer, unsafe.Pointer) bool  //比较两个类型是否相同的equal函数
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte      //GC相关信息
    str       nameOff    //str用来表示类型名称字符串在编译后二进制文件中某个Section的偏移量,由链接器负责填充
    ptrToThis typeOff    //ptrToThis用来表示类型元信息的指针在编译后二进制文件中某个Section的偏移量,由链接器负责填充
}

_type 包含所有类型的共同元信息,编译器和运行时可以根据该元信息解析具体类型、类型名存放位置、类型的Hash值等基本信息。

这里需要说明一下:_type 里面的 nameOff 和 typeOff 最终是由链接器负责确定和填充的,它们都是一个偏移量(offset),类型的名称和类型元信息实际上存放在连接后可执行文件的某个段(section)里,这两个值是相对于段内的偏移量,运行时提供了两个转换查找函数。

// src/runtime/type.go
//获取 _type 的name
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
//获取 _type 的副本
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}

<注意> Go语言类型元信息最初由编译器负责构建,并以表的形式存放在编译后的对象文件中,再由链接器在链接时进行段合并、符号重定向(填充某些值)。这些类型信息在接口的动态调用和反射中被运行时调用。

接口的类型元信息数据结构。代码如下:

// src/runtime/type.go
//描述接口类型的数据结构
type interfacetype struct {
    typ     _type          //类型通用部分
    pkgpath name           //接口所属的包的名字信息,name内存放的不仅有名称,还有描述信息
    expMethods []imethod   //接口的方法
}

//接口方法元信息数据结构
type imethod struct {
    name nameOff    //方法名在编译后的section里面的偏移量
    ityp typeOff    //方法类型在编译后的section里面的偏移量
}

 非空接口的数据结构脉络图

2 接口调用过程分析

前面讨论了接口内部的基本数据结构,本节就来跟踪接口实例化和动态调用的过程,使用Go源码和反汇编代码相结合的方式进行研究。示例代码如下:demo.go

package main

type Caler interface {
    Add(a, b int) int
    Sub(a, b int) int
}

type Adder struct {
    id int
}

func (adder Adder) Add(a, b int) int {
    return a + b
}

func (adder Adder) Sub(a, b int) int {
    return a-b
}

func main(){
    a := Adder{id: 1234}  //1234 = 0x4d2
    var c Caler
    c = a
    
    c.Add(10, 20)
    c.Sub(30, 40)
}

执行下面的命令:

$ go build -gcflags '-l -N' demo.go             #编译生成可执行文件demo

<说明> -gcflags 垃圾回收选项,-l参数代表禁止内联,-N参数代表禁止优化。go在编译目标程序的时候会嵌入运行时(runtime)的二进制,
禁止内联和优化可以让运行时(runtime)中的函数变得更容易调试。

$ go tool objdump -s "main.main" demo   #反汇编可执行文件

<说明> 查看用法:go doc objdump。

TEXT main.main(SB) demo.go
1   demo.go:20            0x45db00                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
2   demo.go:20            0x45db09                483b6110                CMPQ 0x10(CX), SP
3   demo.go:20            0x45db0d                0f86a4000000            JBE 0x45dbb7
4   demo.go:20            0x45db13                4883ec48                SUBQ $0x48, SP
5   demo.go:20            0x45db17                48896c2440              MOVQ BP, 0x40(SP)
6   demo.go:20            0x45db1c                488d6c2440              LEAQ 0x40(SP), BP
7   demo.go:21            0x45db21                48c744242000000000      MOVQ $0x0, 0x20(SP)
8   demo.go:21            0x45db2a                48c7442420d2040000      MOVQ $0x4d2, 0x20(SP)
9   demo.go:22            0x45db33                0f57c0                  XORPS X0, X0
10  demo.go:22            0x45db36                0f11442430              MOVUPS X0, 0x30(SP)
11  demo.go:23            0x45db3b                488b442420              MOVQ 0x20(SP), AX
12  demo.go:23            0x45db40                48890424                MOVQ AX, 0(SP)
13  demo.go:23            0x45db44                e837b1faff              CALL runtime.convT64(SB)
14  demo.go:23            0x45db49                488b442408              MOVQ 0x8(SP), AX
15  demo.go:23            0x45db4e                4889442428              MOVQ AX, 0x28(SP)
16  demo.go:23            0x45db53                488d0dc6c30200          LEAQ go.itab.main.Adder,main.Caler(SB), CX
17  demo.go:23            0x45db5a                48894c2430              MOVQ CX, 0x30(SP)
18  demo.go:23            0x45db5f                4889442438              MOVQ AX, 0x38(SP)
19  demo.go:25            0x45db64                8401                    TESTB AL, 0(CX)
20  demo.go:25            0x45db66                488b0dcbc30200          MOVQ go.itab.main.Adder,main.Caler+24(SB), CX
21  demo.go:25            0x45db6d                48890424                MOVQ AX, 0(SP)
22  demo.go:25            0x45db71                48c74424080a000000      MOVQ $0xa, 0x8(SP)
23  demo.go:25            0x45db7a                48c744241014000000      MOVQ $0x14, 0x10(SP)
24  demo.go:25            0x45db83                ffd1                    CALL CX
25  demo.go:26            0x45db85                488b442430              MOVQ 0x30(SP), AX
26  demo.go:26            0x45db8a                8400                    TESTB AL, 0(AX)
27  demo.go:26            0x45db8c                488b4020                MOVQ 0x20(AX), AX
28  demo.go:26            0x45db90                488b4c2438              MOVQ 0x38(SP), CX
29  demo.go:26            0x45db95                48890c24                MOVQ CX, 0(SP)
30  demo.go:26            0x45db99                48c74424081e000000      MOVQ $0x1e, 0x8(SP)
31  demo.go:26            0x45dba2                48c744241028000000      MOVQ $0x28, 0x10(SP)
32  demo.go:26            0x45dbab                ffd0                    CALL AX
33  demo.go:27            0x45dbad                488b6c2440              MOVQ 0x40(SP), BP
34  demo.go:27            0x45dbb2                4883c448                ADDQ $0x48, SP
35  demo.go:27            0x45dbb6                c3                      RET
36  demo.go:20            0x45dbb7                e844aeffff              CALL runtime.morestack_noctxt(SB)
37  demo.go:20            0x45dbbc                0f1f4000                NOPL 0(AX)
38  demo.go:20            0x45dbc0                e93bffffff              JMP main.main(SB)

分析main函数的汇编代码如下:

3   demo.go:20            0x45db0d                0f86a4000000            JBE 0x45dbb7
4   demo.go:20            0x45db13                4883ec48                SUBQ $0x48, SP
5   demo.go:20            0x45db17                48896c2440              MOVQ BP, 0x40(SP)
6   demo.go:20            0x45db1c                488d6c2440              LEAQ 0x40(SP), BP

为main函数开辟堆栈空间并保存原来的BP指针,这是函数调用前编译器的固定动作。

7   demo.go:21            0x45db21                48c744242000000000      MOVQ $0x0, 0x20(SP)
8   demo.go:21            0x45db2a                48c7442420d2040000      MOVQ $0x4d2, 0x20(SP)

(gdb) p a
$1 = {id = 1234}

汇编代码的第7行和第8行,在堆栈上初始化局部对象,先初始化为0,再初始化为1234(0x4d2)。

9   demo.go:22            0x45db33                0f57c0                  XORPS X0, X0
10  demo.go:22            0x45db36                0f11442430              MOVUPS X0, 0x30(SP)

(gdb) p c
$2 = {tab = 0x0, data = 0x0}

汇编代码第9行和第10行,声明接口变量c并初始化其默认值。

接口的初始化

11  demo.go:23            0x45db3b                488b442420              MOVQ 0x20(SP), AX
12  demo.go:23            0x45db40                48890424                MOVQ AX, 0(SP)
13  demo.go:23            0x45db44                e837b1faff              CALL runtime.convT64(SB)
14  demo.go:23            0x45db49                488b442408              MOVQ 0x8(SP), AX
15  demo.go:23            0x45db4e                4889442428              MOVQ AX, 0x28(SP)
16  demo.go:23            0x45db53                488d0dc6c30200          LEAQ go.itab.main.Adder,main.Caler(SB), CX
17  demo.go:23            0x45db5a                48894c2430              MOVQ CX, 0x30(SP)
18  demo.go:23            0x45db5f                4889442438              MOVQ AX, 0x38(SP)

汇编代码11~18行,对应的是Go代码的 c = a 语句,即接口初始化过程。

使用gdb调试如下:

(gdb) n
25          c.Add(10, 20)
(gdb) p c
$1 = {tab = 0x489f20 <Adder,main.Caler>, data = 0xc0000140a8}
(gdb) p a
$2 = {id = 1234}
(gdb) p &a                         #Adder类型变量a的地址
$27 = (main.Adder *) 0xc00002e758
(gdb) p &c
$7 = (main.Caler *) 0xc00002e768
(gdb) p c.tab                      #iface结构的tab指向的是itab结构
$3 = (runtime.itab *) 0x489f20 <Adder,main.Caler>
(gdb) p c.tab.inter
$4 = (runtime.interfacetype *) 0x468d00
(gdb) p c.tab._type
$5 = (runtime._type *) 0x46b820
(gdb) p c.data                     #data指向的是Adder类型变量a的副本
$12 = (void *) 0xc0000140a8
(gdb) x /1dw 0xc0000140a8
0xc0000140a8:   1234               #该处存储的Adder结构体成员变量id的初始化值1234

 接口初始化工作的结构流程图如下:

通过上面的流程图,可以很清晰的看出,在对非空接口变量进行赋值初始化时的内存分布。通过调用runtime.convT64(),该函数的返回值是一个iface数据结构。其tab指针指向一个itab结构,在将Adder类型变量a赋值给Caler接口变量c时,通过这个结构体将Adder类型和Caler类型关联起来。iface结构的data指针指向的Adder类型变量a的副本。

itab结构的inter指针指向的是一个描述接口类型的interfacetype结构,_type指针指向的是描述结构体类型的 _type结构。

方法的调用

19  demo.go:25            0x45db64                8401                    TESTB AL, 0(CX)
20  demo.go:25            0x45db66                488b0dcbc30200          MOVQ go.itab.main.Adder,main.Caler+24(SB), CX
21  demo.go:25            0x45db6d                48890424                MOVQ AX, 0(SP)
22  demo.go:25            0x45db71                48c74424080a000000      MOVQ $0xa, 0x8(SP)
23  demo.go:25            0x45db7a                48c744241014000000      MOVQ $0x14, 0x10(SP)
24  demo.go:25            0x45db83                ffd1                    CALL CX
25  demo.go:26            0x45db85                488b442430              MOVQ 0x30(SP), AX
26  demo.go:26            0x45db8a                8400                    TESTB AL, 0(AX)
27  demo.go:26            0x45db8c                488b4020                MOVQ 0x20(AX), AX
28  demo.go:26            0x45db90                488b4c2438              MOVQ 0x38(SP), CX
29  demo.go:26            0x45db95                48890c24                MOVQ CX, 0(SP)
30  demo.go:26            0x45db99                48c74424081e000000      MOVQ $0x1e, 0x8(SP)
31  demo.go:26            0x45dba2                48c744241028000000      MOVQ $0x28, 0x10(SP)
32  demo.go:26            0x45dbab                ffd0                    CALL AX
(gdb) p &c.tab.inter
$34 = (runtime.interfacetype **) 0x489f20 <Adder,main.Caler>
(gdb) p &c.tab.fun
$35 = ([1]uintptr *) 0x489f38 <Adder,main.Caler+24>
(gdb) p /x c.tab.fun
$36 = {0x45dbe0}

可以看到itab结构的fun字段的偏移量=0x489f38-0x489f20=0x18,即10进制的24。其存放的是Add()函数的首地址。

至此,整个接口的动态调用完成。从中可以清除地看到,接口的动态调用分为两个阶段:

  • 第1阶段就是构建 iface动态数据结构,这一阶段是在接口初始化或者说实例化的时候完成的,映射到Go语言就是 c = a。
  • 第2阶段就是通过函数指针间接调用接口绑定的实例方法的过程,映射到Go语言就是 c.Add(10, 20) 和 c.Sub(30, 40)。

3 接口调用代价

上面分析了接口动态调用过程,这个过程有两部分多余时耗,一个是接口实例化的过程,也就是 iface 动态结构建立的过程,一旦实例化后,这个接口和具体类型的 itab 数据结构是可以复用的;另一个是接口的方法调用,它是一个函数指针的间接调用过程。

4 空接口数据结构

我们已经知道了空接口是没有任何方法集的接口,所以空接口内部不需要维护与动态内存分配相关的数据结构 itab。空接口只关心存放的具体类型是什么,具体类型的值是什么。所以空接口的底层数据结构也比较简单些。

// src/runtime/runtime2.go
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

从eface 的数据结构可以看出,空接口不是真的为空,其保留了具体实例的类型和值拷贝。即便存放的具体类型是空的,空接口也不是空的。

由于空接口自身没有方法集,所以空接口变量实例化后的真正用途不是接口方法的动态调用。空接口中真正的意义是支持多态。有如下几种方式使用了空接口:

(1)通过接口类型断言。

(2)通过接口类型查询。

(3)通过反射。

参考

《Go语言核心编程》

go的接口内部实现

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值