接口的构造过程是怎样的
我们已经看过了 ifaceeface 的源码,知道 iface 最重要的是 itab_type
为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。
来看一个示例代码:
1
package main
2
3
import "fmt"
4
5
type Person interface {
6
growUp()
7
}
8
9
type Student struct {
10
age int
11
}
12
13
func (p Student) growUp() {
14
p.age += 1
15
return
16
}
17
18
func main() {
19
var qcrao = Person(Student{age: 18})
20
21
fmt.Println(qcrao)
22
}
Copied!
执行命令:
1
go tool compile -S main.go
Copied!
得到 main 函数的汇编代码如下:
1
0x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0
2
0x0000 00000 (./src/main.go:30) MOVQ (TLS), CX
3
0x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX)
4
0x000d 00013 (./src/main.go:30) JLS 157
5
0x0013 00019 (./src/main.go:30) SUBQ $80, SP
6
0x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP)
7
0x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP
8
0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
9
0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
10
0x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP)
11
0x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX
12
0x0031 00049 (./src/main.go:31) MOVQ AX, (SP)
13
0x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX
14
0x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP)
15
0x003f 00063 (./src/main.go:31) PCDATA $0, $0
16
0x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB)
17
0x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX
18
0x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX
19
0x004e 00078 (./src/main.go:33) TESTQ CX, CX
20
0x0051 00081 (./src/main.go:33) JEQ 87
21
0x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX
22
0x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP)
23
0x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP)
24
0x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP)
25
0x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP)
26
0x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX
27
0x0078 00120 (./src/main.go:33) MOVQ AX, (SP)
28
0x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP)
29
0x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP)
30
0x008e 00142 (./src/main.go:33) PCDATA $0, $1
31
0x008e 00142 (./src/main.go:33) CALL fmt.Println(SB)
32
0x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP
33
0x0098 00152 (./src/main.go:34) ADDQ $80, SP
34
0x009c 00156 (./src/main.go:34) RET
35
0x009d 00157 (./src/main.go:34) NOP
36
0x009d 00157 (./src/main.go:30) PCDATA $0, $-1
37
0x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB)
38
0x00a2 00162 (./src/main.go:30) JMP 0
Copied!
我们从第 10 行开始看,如果不理解前面几行汇编代码的话,可以回去看看公众号前面两篇文章,这里我就省略了。
汇编行数
操作
10-14
构造调用 runtime.convT2I64(SB) 的参数
我们来看下这个函数的参数形式:
1
func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
2
// ……
3
}
Copied!
convT2I64 会构造出一个 inteface,也就是我们的 Person 接口。
第一个参数的位置是 (SP),这里被赋上了 go.itab."".Student,"".Person(SB) 的地址。
我们从生成的汇编找到:
1
go.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
Copied!
size=40 大小为40字节,回顾一下:
1
type 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
}
Copied!
把每个字段的大小相加,itab 结构体的大小就是 40 字节。上面那一串数字实际上是 itab 序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 da 9f 20 d4 实际上是 itabhash 值,这在判断两个类型是否相同的时候会用到。
下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 type."".Person 的地址,对应 itab 里的 inter 字段,表示接口类型;8-16 字节最终存储的是 type."".Student 的地址,对应 itab_type 字段,表示具体类型。
第二个参数就比较简单了,它就是数字 18 的地址,这也是初始化 Student 结构体的时候会用到。
汇编行数
操作
15
调用 runtime.convT2I64(SB)
具体看下代码:
1
func 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
}
Copied!
这块代码比较简单,把 tab 赋给了 ifacetab 字段;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 值?
这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下:
1
type iface struct {
2
tab *itab
3
data unsafe.Pointer
4
}
5
type itab struct {
6
inter uintptr
7
_type uintptr
8
link uintptr
9
hash uint32
10
_ [4]byte
11
fun [1]uintptr
12
}
13
14
func 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
}
Copied!
定义了一个山寨版ifaceitab,说它山寨是因为 itab 里的一些关键数据结构都不具体展开了,比如 _type,对比一下正宗的定义就可以发现,但是山寨版依然能工作,因为 _type 就是一个指针而已嘛。
main 函数里,先构造出一个接口对象 qcrao,然后强制类型转换,最后读取出 hash 值,非常妙!你也可以自己动手试一下。
运行结果:
1
iface.tab.hash = 0xd4209fda
Copied!
值得一提的是,构造接口 qcrao 的时候,即使我把 age 写成其他值,得到的 hash 值依然不变的,这应该是可以预料的,hash 值只和他的字段、方法相关。

参考资料

Copy link
Contents
参考资料