go 接口学习笔记


这里是对接口在汇编层面上转换和实现的小结,详细了解可参考 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. 结构体初始化;
  2. 结构体到接口类型转换;
  3. 调用结构体方法;

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. 结构体到接口类型转换;
  3. 调用结构体方法;

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. 总结:

本篇学习笔记大致介绍了接口和结构体类型的互相转换过程,通过汇编代码分析转换的底层逻辑实现知其然,知其所以然。


posted @ 2022-03-30 00:02  lubanseven  阅读(114)  评论(0编辑  收藏  举报