参考资料
参考资料
我们已经看过了 iface
和 eface
的源码,知道 iface
最重要的是 itab
和 _type
。
为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。
来看一个示例代码:
1package main
2
3import "fmt"
4
5type Person interface {
6 growUp()
7}
8
9type Student struct {
10 age int
11}
12
13func (p Student) growUp() {
14 p.age += 1
15 return
16}
17
18func main() {
19 var qcrao = Person(Student{age: 18})
20
21 fmt.Println(qcrao)
22}
23
执行命令:
1go tool compile -S main.go
得到 main 函数的汇编代码如下:
10x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0
20x0000 00000 (./src/main.go:30) MOVQ (TLS), CX
30x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX)
40x000d 00013 (./src/main.go:30) JLS 157
50x0013 00019 (./src/main.go:30) SUBQ $80, SP
60x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP)
70x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP
80x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
90x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
100x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP)
110x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX
120x0031 00049 (./src/main.go:31) MOVQ AX, (SP)
130x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX
140x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP)
150x003f 00063 (./src/main.go:31) PCDATA $0, $0
160x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB)
170x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX
180x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX
190x004e 00078 (./src/main.go:33) TESTQ CX, CX
200x0051 00081 (./src/main.go:33) JEQ 87
210x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX
220x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP)
230x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP)
240x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP)
250x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP)
260x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX
270x0078 00120 (./src/main.go:33) MOVQ AX, (SP)
280x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP)
290x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP)
300x008e 00142 (./src/main.go:33) PCDATA $0, $1
310x008e 00142 (./src/main.go:33) CALL fmt.Println(SB)
320x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP
330x0098 00152 (./src/main.go:34) ADDQ $80, SP
340x009c 00156 (./src/main.go:34) RET
350x009d 00157 (./src/main.go:34) NOP
360x009d 00157 (./src/main.go:30) PCDATA $0, $-1
370x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB)
380x00a2 00162 (./src/main.go:30) JMP 0
我们从第 10 行开始看,如果不理解前面几行汇编代码的话,可以回去看看公众号前面两篇文章,这里我就省略了。
汇编行数 | 操作 |
---|---|
10-14 | 构造调用 runtime.convT2I64(SB) 的参数 |
我们来看下这个函数的参数形式:
1func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
2 // ……
3}
convT2I64
会构造出一个 inteface
,也就是我们的 Person
接口。
第一个参数的位置是 (SP)
,这里被赋上了 go.itab."".Student,"".Person(SB)
的地址。
我们从生成的汇编找到:
1go.itab."".Student,"".Person SNOPTRDATA dupok size=40
2 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
3 0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4
4 rel 0+8 t=1 type."".Person+0
5 rel 8+8 t=1 type."".Student+0
size=40
大小为40字节,回顾一下:
1type itab struct {
2 inter *interfacetype // 8字节
3 _type *_type // 8字节
4 link *itab // 8字节
5 hash uint32 // 4字节
6 bad bool // 1字节
7 inhash bool // 1字节
8 unused [2]byte // 2字节
9 fun [1]uintptr // variable sized // 8字节
10}
把每个字段的大小相加,itab
结构体的大小就是 40 字节。上面那一串数字实际上是 itab
序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 da 9f 20 d4
实际上是 itab
的 hash
值,这在判断两个类型是否相同的时候会用到。
下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 type."".Person
的地址,对应 itab
里的 inter
字段,表示接口类型;8-16 字节最终存储的是 type."".Student
的地址,对应 itab
里 _type
字段,表示具体类型。
第二个参数就比较简单了,它就是数字 18
的地址,这也是初始化 Student
结构体的时候会用到。
汇编行数 | 操作 |
---|---|
15 | 调用 runtime.convT2I64(SB) |
具体看下代码:
1func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
2 t := tab._type
3
4 //...
5
6 var x unsafe.Pointer
7 if *(*uint64)(elem) == 0 {
8 x = unsafe.Pointer(&zeroVal[0])
9 } else {
10 x = mallocgc(8, t, false)
11 *(*uint64)(x) = *(*uint64)(elem)
12 }
13 i.tab = tab
14 i.data = x
15 return
16}
这块代码比较简单,把 tab
赋给了 iface
的 tab
字段;data
部分则是在堆上申请了一块内存,然后将 elem
指向的 18
拷贝过去。这样 iface
就组装好了。
汇编行数 | 操作 |
---|---|
17 | 把 i.tab 赋给 CX |
18 | 把 i.data 赋给 AX |
19-21 | 检测 i.tab 是否是 nil,如果不是的话,把 CX 移动 8 个字节,也就是把 itab 的 _type 字段赋给了 CX,这也是接口的实体类型,最终要作为 fmt.Println 函数的参数 |
后面,就是调用 fmt.Println
函数及之前的参数准备工作了,不再赘述。
这样,我们就把一个 interface
的构造过程说完了。
【引申1】
如何打印出接口类型的 Hash
值?
这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下:
1type iface struct {
2 tab *itab
3 data unsafe.Pointer
4}
5type itab struct {
6 inter uintptr
7 _type uintptr
8 link uintptr
9 hash uint32
10 _ [4]byte
11 fun [1]uintptr
12}
13
14func main() {
15 var qcrao = Person(Student{age: 18})
16
17 iface := (*iface)(unsafe.Pointer(&qcrao))
18 fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash)
19}
定义了一个山寨版
的 iface
和 itab
,说它山寨
是因为 itab
里的一些关键数据结构都不具体展开了,比如 _type
,对比一下正宗的定义就可以发现,但是山寨版
依然能工作,因为 _type
就是一个指针而已嘛。
在 main
函数里,先构造出一个接口对象 qcrao
,然后强制类型转换,最后读取出 hash
值,非常妙!你也可以自己动手试一下。
运行结果:
1iface.tab.hash = 0xd4209fda
值得一提的是,构造接口 qcrao
的时候,即使我把 age
写成其他值,得到的 hash
值依然不变的,这应该是可以预料的,hash
值只和他的字段、方法相关。
参考资料
【曹大神翻译的文章,非常硬核】http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable