这里是对接口在汇编层面上转换和实现的小结,详细了解可参考 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. 总结:

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


go 接口学习笔记的更多相关文章

  1. JMeter接口学习笔记2017

    协议学习地址:http://www.cnblogs.com/TankXiao/archive/2012/02/13/2342672.html 本篇学习笔记来自于慕课网上学习JMeter的学习笔记 学习 ...

  2. PHP 开发 APP 接口 学习笔记与总结 - Redis 缓存

    Redis 可以定期将数据备份到磁盘中(持久化),同时不仅仅支持简单的key/value 类型的数据,同时还提供list,set,hash等数据结构的存储:Memcache 只是简单的key/valu ...

  3. thinkphp5开发restful-api接口 学习笔记一

    视频学习地址: http://study.163.com/course/courseMain.htm?courseId=1004171002 源码和文档(如果满意,欢迎 star): https:// ...

  4. 工程化编程实战callback接口学习笔记

    一.编译并运行 help.version命令执行正常,但quit命令出错 二.Debug 从命令输入到执行过程: 源代码: 更改后: 运行结果:能正确运行quit命令 Callback接口学习成果: ...

  5. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [3] 首页 APP 接口开发方案 ② 读取缓存方式

    以静态缓存为例. 修改 file.php line:11 去掉 path 参数(方便),加上缓存时间参数: public function cacheData($k,$v = '',$cacheTim ...

  6. OpenCV(C++接口)学习笔记1-图像读取、显示、保存

    OpenCV在2.0加入版本号之后C++接口函数,学习前C语言的接口功能.现在OpenCV它已被发展到2.4.9版本号,所以,我决定学习C++接口函数,与步伐. 1.创建图像 cv::Mat imag ...

  7. .net接口学习笔记

    1.接口的声明 接口的声明不能包含:数据成员,静态变量:只能包含如下类型的静态成员函数的声明:方法,属性,事件,索引器.声明中不能包含任何实现的代码,而在每个成员成名的主体后,必须使用分号. 接口声明 ...

  8. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [6] 版本升级接口开发

    判定 app 是否需要加密:通过 app 表中的 status 字段来判定,加密的字符串为 app 表中的 key 字段. 在获取的客户端和服务器端(数据库表中相应字段)的版本号不一致时,返回 dat ...

  9. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [5] 版本设计分析及数据表设计

    APP 版本升级以及 APP 演示 ① 版本升级分析以及数据表设计 ② 版本升级接口开发以及 APP 演示 /** * version_upgrade 版本升级信息表 */ CREATE TABLE ...

  10. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [4] 首页 APP 接口开发方案 ③ 定时读取缓存方式

    用于 linux 执行 crontab 命令生成缓存的文件 crop.php <?php //让crontab 定时执行的脚本程序 require_once 'db.php'; require_ ...

随机推荐

  1. 13 HTTP传输大文件的方法

    目录 如何在有限的带宽下高效快捷传输大文件? 数据压缩 分块传输 范围请求 多段数据 如何在有限的带宽下高效快捷传输大文件? 数据压缩 分块传输 范围请求 多段数据 数据压缩 思路:把大文件整体变小 ...

  2. offline RL | Pessimistic Bootstrapping (PBRL):在 Q 更新中惩罚 uncertainty,拉低 OOD Q value

    论文题目:Pessimistic Bootstrapping for Uncertainty-Driven Offline Reinforcement Learning,ICLR 2022,6 6 8 ...

  3. Win10遇到服务器启动失败 80端口被占用如何解决

    Win10提示"服务器启动失败,80端口被占用"怎么办?具体解决方法如下 步骤如下: 1.以管理员身份运行cmd; 2.输入:net stop http 注:如果提示是否真的需要停 ...

  4. shell中 << EOF 和 EOF 使用

    转载请注明出处: EOF(End of File)在Shell中通常用于指示输入的结束,并在脚本或命令中进行多行输入.它允许用户指定一个特定的分界符来表示输入的结束,通常用于创建临时文件.重定向输入或 ...

  5. MinIO客户端之ping

    MinIO提供了一个命令行程序mc用于协助用户完成日常的维护.管理类工作. 官方资料 mc ping 检查指定的MinIO节点的服务是否可用. 不带参数,命令如下: ./mc ping local1 ...

  6. Jenkins 系列:Jenkins 安装(Windows、Mac、Centos)和简介

    目录 简介 发展历史 应用场景 Jenkins 安装部署 先决条件 硬件要求 软件包下载 war 包部署 linux 系统部署 mac 系统部署 windows 系统部署 安装后基本配置 解锁 自定义 ...

  7. Java的特性、内容和环境的配置

    Java的特性和优势 简单性 面向对象 可移植性 高性能 分布式 动态性 多线程 安全性 健壮性 JDK包含JRE包含JVM JDK:Java Development Kit JRE:Java Run ...

  8. [ACTF2020 新生赛]Exec 1

    [ACTF2020 新生赛]Exec 1 审题 发现题目有ping功能,猜测是命令执行漏洞. 知识点 linux系统命令 解题 先ping127.0.0.1,观察是否正常执行. 发现正常后执行ls / ...

  9. vue模板的首次渲染,和重新渲染,有哪些区别?

    搞明白这个,能帮助我们理解开发中出现的很多问题. 一.我们先来回顾一下vue模板的渲染过程: (1)执行render函数,生成虚拟DOM. render函数是根据render.templete.el这 ...

  10. 文心一言 VS 讯飞星火 VS chatgpt (178)-- 算法导论13.4 1题

    一.用go语言,在执行 RB-DELETE-FIXUP 之后,证明:树根一定是黑色的. 文心一言: 在证明 RB-DELETE-FIXUP 之后,树根一定是黑色的的过程中,我们首先需要理解红黑树的性质 ...