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

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


  • 相关阅读:
    python开发学习-day01 (python安装与版本、字符串、字典、运算符、文件)
    js数组中的常用方法总结
    js与jquery常用数组方法总结
    常用原生JS方法总结(兼容性写法)
    左右切换
    改变奇数行颜色
    toogle
    jQuery 表单验证
    css选择器
    两句话帮你彻底记住gdb之eXamining memory
  • 原文地址:https://www.cnblogs.com/xingzheanan/p/16074570.html
Copyright © 2020-2023  润新知