这里是对接口在汇编层面上转换和实现的小结,详细了解可参考 Go 语言接口的原理
1. 类型转换:结构体到接口
1.1 结构体方法实现接口
package main
type Duck interface {
Quack()
}
type Cat struct {
Name string
}
//go:noinline
func (c Cat) Quack() {
println(c.Name + " handsome")
}
func main() {
var c Duck = Cat{Name: "lubanseven"}
c.Quack()
}
将汇编实现分为三块:
- 结构体初始化;
- 结构体到接口类型转换;
- 调用结构体方法;
1.1.1 结构体初始化
XORPS X0, X0 ;; X0 = 0
MOVUPS X0, ""..autotmp_1+48(SP) ;; StringHeader(SP+48).Data = 0
LEAQ go.string."lubanseven"(SB), AX ;; AX = &"lubanseven"
MOVQ AX, ""..autotmp_1+48(SP) ;; StringHeader(SP+48).Data = AX = &"lubanseven"
MOVQ $10, ""..autotmp_1+56(SP) ;; StringHeader(SP+56).Len = 10
示意图如下:
1.1.2 结构体到接口类型转换
LEAQ go.itab."".Cat,"".Duck(SB), AX ;; AX = itab = &(go.itab."".Cat,"".Duck)
MOVQ AX, (SP) ;; SP = AX
LEAQ ""..autotmp_1+48(SP), AX ;; AX = StringHeader(SP+48).Data
MOVQ AX, 8(SP) ;; SP + 8 = AX
CALL runtime.convT2I(SB) ;; runtime.convT2I(SP, SP+8)
查看 runtime.convT2I 函数的实现:
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
runtime.convT2I 函数会返回 runtime.iface 结构体,该结构体表示包含方法的接口。其中,函数内通过获取的类型分配内存空间,并将 elem 指针指向的内容拷贝到堆中。
返回的 runtime.iface 结构体将放在栈上的 SP+16 ~ SP+32 处,分别表示 iface.tab 和 iface.data。
示意图如下:
1.1.3 调用结构体方法
MOVQ 16(SP), AX
MOVQ 24(SP), CX
MOVQ AX, "".c+32(SP)
MOVQ CX, "".c+40(SP)
MOVQ "".c+32(SP), AX
MOVQ 24(AX), AX ;; AX = *AX + 24 = iface.tab.fun[0] = Cat.Quack()
MOVQ "".c+40(SP), CX ;; CX = iface.data
MOVQ CX, (SP) ;; SP = CX
CALL AX ;; CX.Quack()
其中,MOVQ 24(AX), AX
表示将 iface.tab 中指向方法 Quack() 的指针赋给 AX。由于 Duck 接口只有一个 Quack 方法,因此这里 24(AX) 索引到的即是第一个方法指针。
最后,CALL AX
传递 (SP) 的结构体值,实现 Quack() 方法的调用。
示意图如下:
1.2 结构体指针方法实现接口
package main
type Duck interface {
Quack()
}
type Cat struct {
Name string
}
//go:noinline
func (c *Cat) Quack() {
println(c.Name + " handsome")
}
func main() {
var c Duck = &Cat{Name: "lubanseven"}
c.Quack()
}
同样的,将汇编实现分为三块:
- 结构体初始化;
- 结构体到接口类型转换;
- 调用结构体方法;
1.2.1 结构体初始化
LEAQ type."".Cat(SB), AX ;; AX = &type."".Cat
MOVQ AX, (SP) ;; SP = AX = &type."".Cat
CALL runtime.newobject(SB) ;; SP + 8 = &Cat{}
MOVQ 8(SP), DI ;; DI = SP + 8
MOVQ DI, ""..autotmp_2+16(SP) ;; SP + 16 = DI
MOVQ $10, 8(DI) ;; *DI + 8 = StringHeader(DI.Name).Len = 10
LEAQ go.string."lubanseven"(SB), AX ;; AX = &"lubanseven"
MOVQ AX, (DI) ;; *DI = StringHeader(DI.Name).Data = AX
需要说明的是,LEAQ type."".Cat(SB), AX
将指向类型 Cat 的指针赋给 AX。runtime.newobject(SB)
创建结构体 Cat 的实例。通过 DI 寄存器对结构体变量赋值,注意字符串 string 的结构体实现是 StringHeader{...}。
示意图如下:
1.2.2 结构体到接口类型转换
MOVQ ""..autotmp_2+16(SP), AX
LEAQ go.itab.*"".Cat,"".Duck(SB), CX
MOVQ CX, "".c+32(SP)
MOVQ AX, "".c+40(SP)
结构体到接口类型的转换即转换为接口结构体 runtime.iface
。其中,SP+32 表示 iface.tab,SP+40 表示 iface.data。SP+32 ~ SP+48 共同组成了接口结构体 runtime.iface,实现结构体 Cat 到接口类型的转换。
示意图如下:
1.2.3 调用指针接收者方法
MOVQ "".c+32(SP), AX
MOVQ 24(AX), AX
MOVQ "".c+40(SP), CX
MOVQ CX, (SP)
CALL AX
此例和 1.1.3 节类似,这里不加以描述了。
2. 类型转换:接口到结构体
除了结构体到接口的类型转换,go 也有接口到结构体类型的转换。通过类型断言可以实现,但类型断言背后做了些什么呢?
这里分空接口和非空接口两种情况查看接口到结构体类型转换。
2.1 非空接口
接口到结构体转换示例代码:
func main() {
var c Duck = &Cat{Name: "lubanseven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
}
从汇编代码看 Cat 结构体和接口结构体 runtime.iface 的创建过程类似,这里忽略。直接看最关键的接口类型到结构体类型的转换过程:
00079 LEAQ go.itab.*"".Cat,"".Duck(SB), CX ;; CX = &(go.itab.*"".Cat,"".Duck)
00086 MOVQ CX, "".c+56(SP) ;; SP + 56 = CX
00101 MOVQ "".c+56(SP), CX ;; CX = SP + 56
00125 MOVL 16(CX), AX ;; AX = *CX + 16 = runtime.iface.itab.hash
00132 CMPL AX, $593696792 ;; if runtime.iface.itab.hash == $593696792 {
00137 JEQ 141
00139 JMP 236
00176 MOVQ "".c+64(SP), AX ;; AX = &Cat{Name: "lubanseven"}
00205 MOVQ AX, (SP) ;; SP = AX
00209 CALL "".(*Cat).Quack(SB) ;; SP.Quack()
00214 JMP 216
00236 JMP 228 ;; } else {
00228 JMP 230 ;;
00230 JMP 216 ;;
00216 MOVQ 104(SP), BP ;; BP = SP + 104
00221 ADDQ $112, SP ;; SP = SP + 112
00225 RET ;; }
可以看到,类型转换实际上是通过比较 runtime.iface.itab.hash 和结构体 hash 判断类型是否相等,如果相等调用结构体,实现方法调用。如果不相等,则回收函数栈空间。
2.2 空接口
对于空接口类型转换,编译器省略了将结构体转换为 runtime.eface 的过程,从汇编代码上并未看到转换过程。和非空接口逻辑类似,空接口转换也需判断 hash 值,不过空接口的 hash 从 runtime.eface._type 获取。
3. 总结:
本篇学习笔记大致介绍了接口和结构体类型的互相转换过程,通过汇编代码分析转换的底层逻辑实现知其然,知其所以然。