Go 接口的构造过程
博客Go接口的底层实现源码分析可以看到 iface
和 eface
的源码,知道 iface
最重要的是 itab
和 _type
。
为了研究清楚接口是如何构造的,接下来利用汇编还原背后的真相。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import "fmt"
type Person interface { growUp() }
type Student struct { age int }
func (p Student) growUp() { p.age += 1 return }
func main() { var qcrao = Person(Student{age: 18})
fmt.Println(qcrao) }
|
执行命令:
1
| go tool compile -S main.go
|
得到 main 函数的汇编代码如下:
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
| 0x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0 0x0000 00000 (./src/main.go:30) MOVQ (TLS), CX 0x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX) 0x000d 00013 (./src/main.go:30) JLS 157 0x0013 00019 (./src/main.go:30) SUBQ $80, SP 0x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP) 0x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP 0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB) 0x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP) 0x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX 0x0031 00049 (./src/main.go:31) MOVQ AX, (SP) 0x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX 0x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP) 0x003f 00063 (./src/main.go:31) PCDATA $0, $0 0x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB) 0x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX 0x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX 0x004e 00078 (./src/main.go:33) TESTQ CX, CX 0x0051 00081 (./src/main.go:33) JEQ 87 0x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX 0x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP) 0x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP) 0x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP) 0x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP) 0x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX 0x0078 00120 (./src/main.go:33) MOVQ AX, (SP) 0x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP) 0x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP) 0x008e 00142 (./src/main.go:33) PCDATA $0, $1 0x008e 00142 (./src/main.go:33) CALL fmt.Println(SB) 0x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP 0x0098 00152 (./src/main.go:34) ADDQ $80, SP 0x009c 00156 (./src/main.go:34) RET 0x009d 00157 (./src/main.go:34) NOP 0x009d 00157 (./src/main.go:30) PCDATA $0, $-1 0x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB) 0x00a2 00162 (./src/main.go:30) JMP 0
|
从第 10 行开始看:
汇编行数 |
操作 |
10-14 |
构造调用 runtime.convT2I64(SB) 的参数 |
函数的参数形式:
1 2 3
| func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) { }
|
convT2I64
会构造出一个 inteface
,也就是我们的 Person
接口。
第一个参数的位置是 (SP)
,这里被赋上了 go.itab."".Student,"".Person(SB)
的地址。
我们从生成的汇编找到:
1 2 3 4 5
| go.itab."".Student,"".Person SNOPTRDATA dupok size=40 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4 rel 0+8 t=1 type."".Person+0 rel 8+8 t=1 type."".Student+0
|
size=40
大小为40字节,回顾一下:
1 2 3 4 5 6 7 8 9 10
| type itab struct { inter *interfacetype _type *_type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr }
|
把每个字段的大小相加,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) |
具体看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) { t := tab._type var x unsafe.Pointer if *(*uint64)(elem) == 0 { x = unsafe.Pointer(&zeroVal[0]) } else { x = mallocgc(8, t, false) *(*uint64)(x) = *(*uint64)(elem) } i.tab = tab i.data = x return }
|
这块代码比较简单,把 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
值?
这里参考一篇文章Go和interface探究。具体做法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter uintptr _type uintptr link uintptr hash uint32 _ [4]byte fun [1]uintptr }
func main() { var qcrao = Person(Student{age: 18})
iface := (*iface)(unsafe.Pointer(&qcrao)) fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash) }
|
定义了一个山寨版
的 iface
和 itab
,说它山寨
是因为 itab
里的一些关键数据结构都不具体展开了,比如 _type
,对比一下正宗的定义就可以发现,但是山寨版
依然能工作,因为 _type
就是一个指针而已嘛。
在 main
函数里,先构造出一个接口对象 qcrao
,然后强制类型转换,最后读取出 hash
值!
运行结果:
1
| iface.tab.hash = 0xd4209fda
|
值得一提的是,构造接口 qcrao
的时候,即使把 age
写成其他值,得到的 hash
值依然不变的,这应该是可以预料的,hash
值只和他的字段、方法相关。
参考资料
http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable