前文介绍过golang interface用法,本文详细剖析interface内部实现和细节。
empty interface实现细节
interface底层使用两种类型实现的,一个是eface,一个是iface。当interface中没有方法的时候,底层是通过eface实现的。
当interface包含方法时,那么它的底层是通过iface实现的。
对于iface和eface具体实现在go源码runtime2.go中,我们看下源码
1 | type eface struct { |
可以看到eface包含两个结构,一个是_type类型指针,一个是unsafe包的Pointer变量
继续追踪Pointer
1 | type Pointer *ArbitraryType |
可以看出Pointer实际上是int类型的指针。我们再看看_type类型
1 | type _type struct { |
size 为该类型所占用的字节数量。
kind 表示类型的种类,如 bool、int、float、string、struct、interface 等。
str 表示类型的名字信息,它是一个 nameOff(int32) 类型,通过这个 nameOff,可以找到类型的名字字符串
eface结构总结图
eface 分两个部分, *_type 类型为实际类型转化为type类型的指针,data为实际数据。
具体类型如何转化为eface
我们写一段程序efacedemo.go,然后用gobuild命令生成可执行文件,再用汇编查看下源码。
1 | package main |
先用gcflags标记编译生成可执行文件efacedemo
go build -gcflags “-l” -o efacedemo efacedemo.go
然后执行go tool objdump 将 可执行程序efacedemo中main包的main函数转为汇编代码
go tool objdump -s “main.main” efacedemo
1 | efacedemo.go:12 0x48ffa0 65488b0c2528000000 MOVQ GS:0x28, CX |
抛开寄存器寻址和寄存数据不谈,我们看到efacedemo.go:16行, CALL runtime.convT64(SB)语句说明调用了runtime包的convT64函数
这个函数在runtime.go 中有声明
1 | // Specialized type-to-interface conversion. |
看注释就知道是type类型转化为interface类型,计算机将EmpStruct强制转化为type类型后,type类型进一步转化为interface,并且将EmpStruct数据
转化为unsafe.Pointer,毕竟int在64位机器中为8字节,所以采用了convT64函数。
还有一个函数convT2E,这个函数是将type转化为eface类型,大家可以读一读runtime源码。
convT2E源码
1 | func convT2E(t *_type, elem unsafe.Pointer) (e eface) { |
内部调用了typedmemove做类型判断,所以一个类能否转化为某个接口是在runtime这一层做判断的。判断条件就是我之前所说的是否
实现了该接口所有的方法。
将上面的代码用eface图解就是
具体类型如何转换为iface
当接口中带有方法的时候,接口底层的实现就是通过iface结构实现的。我们下一个带方法的接口,然后反汇编一下。
1 | package main |
和之前操作一样,先编译
go build -gcflags “-l” -o ifacedemo ifacedemo.go
然后反汇编找到指令
go tool objdump -s “main.main” ifacedemo
生成的汇编指令如下
1 | ifacedemo.go:17 0x48ffb0 65488b0c2528000000 MOVQ GS:0x28, CX |
抛开寄存器寻址和移动数据,我们查看call和重要的数据copy
19行LEAQ runtime.types+111360(SB), AX做了类型上的转换
19行CALL runtime.newobject(SB)调用了runtime包的newobject方法,开辟我们结构体的指针
22行MOVQ go.itab.*main.EmpStruct,main.EmpInter+8(SB), CX将我们结构体部分信息保存在itab中。
23行 CALL runtime.convT64(SB), 实际上将64位的num转化为数据存在data域中。
这些函数都可以在runtime包中找到,读者可以自己阅读。另外,还有个重要函数convT2I
1 | func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { |
这个函数将Type类型转化为Iface类型。类似的函数还有convT2Inoptr(Type转Iface指针)。读者可以自己阅读源码。
通过上面的调试和分析,我们知道iface中结构和eface略有不同,多出一个itab类型的结构,我们看看iface源码
1 | type iface struct { |
和eface不同,iface的第一个字段是itab指针。我们继续查看itab的定义
1 | type itab struct { |
itab 各参数含义
inter和_type共同确认实际的类型信息,因为在接口中有方法的时候,itab要保存接口方法的一些额外信息,如名字,类型等。
hash 用于查询类型和判断类型,比如接口赋值,接口转换判断等。
fun 为具体的方法集,所有的方法保存在fun里。虽然fun大小为1,但是这其实是一个柔型数组,后面的地址空间连续且安全,
后面能存多少函数,取决于itab初始化多大的空间。
图解iface结构
结合代码,具象化的绘制一下
以上就是interface内部结构和动态调用的原理。根据fun方法集动态调用具体类的方法,从而实现了多态。
gdb调试
除了可以通过反汇编的方式查看代码指令,其实通过汇编查看函数调用也是很不错的手段。
go build -gcflags “-N -l” -o ifacedemo ifacedemo.go
先编译出可执行文件, -N 忽略优化
然后gdb调试
gdb ifacedemo
进入gdb调试界面
1
2
3
4
5
6
7
8
9
10
11 (gdb) list 20
15 }
16
17 func main() {
18
19 emps := EmpStruct{num: 1}
20 var empi EmpInter
21 empi = &emps
22 fmt.Println(empi)
23 fmt.Println(emps)
24
输入list 20 为了列举 20行左右代码,然后我们在22行处打个断点,执行run让程序跑在断点处停止
1
2
3
4
5
6
7
8
9
10(gdb) break 22
Breakpoint 1 at 0x4872ea: file /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo.go, line 22.
(gdb) run
Starting program: /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo
[New LWP 9562]
[New LWP 9563]
[New LWP 9564]
Thread 1 "ifacedemo" hit Breakpoint 1, main.main () at /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo.go:22
22 fmt.Println(empi)
接下来我们查看下empi这个接口的数据信息
1
2(gdb) p empi
$1 = {tab = 0x4d0ee0 <EmpStruct,main.EmpInter>, data = 0xc000078010}
看得出empi是iface结构的,包含tab和data两个字段。
我们查看下tab里的内容
1
2
3
4(gdb) p empi.tab
$2 = (runtime.itab *) 0x4d0ee0 <EmpStruct,main.EmpInter>
(gdb) p *empi.tab
$3 = {inter = 0x4a17a0, _type = 0x49f3c0, hash = 4144246241, _ = "\000\000\000", fun = {4747840}}
可以看得出 tab是个地址,我们*解引用看到内部内容和上面所述一样,inter, _type, hash, 函数集合fun
接下来我们将代码反汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 (gdb) disass
Dump of assembler code for function main.main:
0x0000000000487260 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000487269 <+9>: lea -0x58(%rsp),%rax
0x000000000048726e <+14>: cmp 0x10(%rcx),%rax
0x0000000000487272 <+18>: jbe 0x487443 <main.main+483>
0x0000000000487278 <+24>: sub $0xd8,%rsp
0x000000000048727f <+31>: mov %rbp,0xd0(%rsp)
0x0000000000487287 <+39>: lea 0xd0(%rsp),%rbp
0x000000000048728f <+47>: lea 0x1b22a(%rip),%rax # 0x4a24c0
0x0000000000487296 <+54>: mov %rax,(%rsp)
0x000000000048729a <+58>: callq 0x40b070 <runtime.newobject>
0x000000000048729f <+63>: mov 0x8(%rsp),%rax
0x00000000004872a4 <+68>: mov %rax,0x68(%rsp)
0x00000000004872a9 <+73>: movq $0x0,0x30(%rsp)
0x00000000004872b2 <+82>: movq $0x1,0x30(%rsp)
0x00000000004872bb <+91>: mov 0x68(%rsp),%rax
0x00000000004872c0 <+96>: movq $0x1,(%rax)
0x00000000004872c7 <+103>: xorps %xmm0,%xmm0
0x00000000004872ca <+106>: movups %xmm0,0x70(%rsp)
0x00000000004872cf <+111>: mov 0x68(%rsp),%rax
0x00000000004872d4 <+116>: mov %rax,0x50(%rsp)
0x00000000004872d9 <+121>: lea 0x49c00(%rip),%rcx # 0x4d0ee0 <go.itab.*main.EmpStruct,main.EmpInter>
0x00000000004872e0 <+128>: mov %rcx,0x70(%rsp)
0x00000000004872e5 <+133>: mov %rax,0x78(%rsp)
0x00000000004872ea <+138>: mov 0x78(%rsp),%rax
0x00000000004872ef <+143>: mov 0x70(%rsp),%rcx
0x00000000004872f4 <+148>: mov %rcx,0x80(%rsp)
0x00000000004872fc <+156>: mov %rax,0x88(%rsp)
0x0000000000487304 <+164>: mov %rcx,0x48(%rsp)
0x0000000000487309 <+169>: cmpq $0x0,0x48(%rsp)
0x000000000048730f <+175>: jne 0x487316 <main.main+182>
0x0000000000487311 <+177>: jmpq 0x48743e <main.main+478>
0x0000000000487316 <+182>: test %al,(%rcx)
0x0000000000487318 <+184>: mov 0x8(%rcx),%rax
0x000000000048731c <+188>: mov %rax,0x48(%rsp)
0x0000000000487321 <+193>: jmp 0x487323 <main.main+195>
0x0000000000487323 <+195>: xorps %xmm0,%xmm0
0x0000000000487326 <+198>: movups %xmm0,0x90(%rsp)
0x000000000048732e <+206>: lea 0x90(%rsp),%rax
0x0000000000487336 <+214>: mov %rax,0x40(%rsp)
0x000000000048733b <+219>: test %al,(%rax)
0x000000000048733d <+221>: mov 0x48(%rsp),%rcx
0x0000000000487342 <+226>: mov 0x88(%rsp),%rdx
0x000000000048734a <+234>: mov %rcx,0x90(%rsp)
0x0000000000487352 <+242>: mov %rdx,0x98(%rsp)
0x000000000048735a <+250>: test %al,(%rax)
0x000000000048735c <+252>: jmp 0x48735e <main.main+254>
0x000000000048735e <+254>: mov %rax,0xa0(%rsp)
0x0000000000487366 <+262>: movq $0x1,0xa8(%rsp)
0x0000000000487372 <+274>: movq $0x1,0xb0(%rsp)
0x000000000048737e <+286>: mov %rax,(%rsp)
0x0000000000487382 <+290>: movq $0x1,0x8(%rsp)
0x000000000048738b <+299>: movq $0x1,0x10(%rsp)
0x0000000000487394 <+308>: callq 0x480c60 <fmt.Println>
=> 0x0000000000487399 <+313>: mov 0x68(%rsp),%rax
0x000000000048739e <+318>: mov (%rax),%rax
0x00000000004873a1 <+321>: mov %rax,0x38(%rsp)
0x00000000004873a6 <+326>: mov %rax,(%rsp)
0x00000000004873aa <+330>: callq 0x408950 <runtime.convT64>
0x00000000004873af <+335>: mov 0x8(%rsp),%rax
---Type <return> to continue, or q <return> to quit---
显示的就是在main包main函数的反汇编。和我们之前用tool工具看到的一样。
到此为止,interface内部结构和特性介绍完毕
感谢关注我的公众号