【Go】内存中的空接口

第一次接触Go程序,是一个朋友想让帮忙看看那个可执行文件实现了什么功能。

当时打开看了一眼就蒙圈了,看起来和C/C++程序编译的可执行文件完全不一样,连程序的入口都不一样。

很久之后,因工作需要,使用Golang语言进行开发,Golang很多特性引起了我的兴趣。

简洁的语法使得开发工作变得高效,就像甜甜的糖果吃起来很美味,但是它是怎么运行的呢?

于是从简单的开始,深入探索其运行时,审视其最终的样子。

环境

OS : Ubuntu 20.04.2 LTS; x86_64Go : go version go1.16.2 linux/amd64

操作系统不同、处理器架构不同、Golang版本不同均可导致内存分配不同,因此不保证本文中分析结果在其它环境下也始终一致。

空接口类型

interface{} 称为空接口。

图片

Golang 的 HelloWorld 程序使用以下方法打印字符串到标准输出。

图片

其中 interface{} 类型的形式参数可以接收任何类型的数据作为实际参数。

这是一件很神奇的事。

而且 interface{} 类型的变量可以引用任何类型的数据,代码如下:

图片

在Java语言中,Object类型的变量可以引用任何类型的对象;因为任何Java类都 extends java.lang.Object,运行时生成的动态代理类也不例外。

在C语言中,void * 号称万能指针, 表示一个内存地址引用,可以指向任何类型的数据,但是只能接收指针类型的赋值操作。

在Go语言中,interface{} 就厉害了,任何类型数据都敢接、都能接。

代码清单

图片

备注:

编译器为了提高程序运行效率,减少寻址和内存跳转,会对代码进行优化;

PrintInterface函数体太小,在编译之后二进制可执行文件中,不会被分配单独的内存空间;

使用//go:noinline禁止编译器对该函数进行优化。

定义PrintInterface函数是为了更清晰的查看数据类型的转变、参数传递,也就是查看int类型数据如何转变为interface{}类型数据,以及interface{}数据在内存中是什么样子。

编译代码

代码清单中,定义了两个函数。编译过程中它们的名字被重新定义,如图中红框标记所示。

图片

编译后的函数名称由两部分组成:

  • "."之前的部分(main)是原包名

  • "."之后的部分(main和PrintInterface)是原函数名

深入内存

函数 main

图片

上图就是编译之后的main函数。三处标记描述如下:

①分配栈帧

main函数的栈帧分配非常小,共0x18个字节。在64位操作系统中,也就三个寄存器大小。执行①指令后,main函数栈帧结构如下图蓝色的区域所示:

图片

②把123放入内存

movq   $0x7b,(%rsp)

该指令将123放入栈顶。

图片

紧接着调用了 runtime.convT64 函数;

而在源代码清单中,并没有调用该函数,怎么编译后多了个函数调用呢?

实际上,runtime.convT64 函数只是把一个64位的无符号整数转变为一个指向该整数值的指针,源码如下图所示,逻辑非常简单易懂:

图片

代码中,runtime.staticuint64s 缓存了[0,256)之间的整数,减少动态分配内存,减少垃圾回收。

我们再看一下编译后的 runtime.convT64 函数,执行流程和源代码一致;只是我们能更加清晰的了解其执行细节。

图片

因为123(0x7b)小于255(0xff),所以本次调用 runtime.convT64 函数虽然分配了栈帧,但是并没有使用该栈帧存储数据。

在 runtime.convT64 函数执行完成返回 main 函数之前,将函数返回值540AB8保存到 main 函数的栈帧中,方法调用栈如下图所示:

图片

虽然 runtime.convT64 函数只有一个参数,仍然使用栈内存传递,而不是使用寄存器(例如:rdi、rsi、rdx、rcx、r8、r9)。

虽然 runtime.convT64 函数只有一个返回值,仍然没有是使用寄存器(例如:rax)保存返回值,而是直接把返回值写入调用者的栈帧内。

这样做有很明显的好处:

  • 方便实现函数的多个返回值存储;

  • 方便函数的调用者直接使用其返回值。

在本例中,返回值紧挨着参数保存。所有调用 runtime.convT64 函数的地方,都应该是这样。

③调用 PrintInterface 函数

在调用PrintInterface函数之前,两条指令将4A2140保存到栈顶,覆盖了原值123。​​​​​​​

lea    0xaacf(%rip),%rax        # 4a2140 <type.*+0xa140>mov    %rax,(%rsp)

由于123已经从栈内转移到了栈外,并可以通过指针值540AB8找到,所以此处覆盖是没问题的。此时,main函数的栈帧如下:

图片

函数 PrintInterface

查看指令如下:

图片

在打印参数到标准输出之前,先从 main 函数的栈帧中读取两个参数,并保存到当前栈帧内存中。

图片

根据调用约定,调用PrintInterface函数时,传递了两个参数:

  • 4A2140作为第一个参数

  • 540AB8作为第二个参数,指向真实的整数值123

而在代码清单中,只传递了一个interface{}类型的参数。

✌✌✌

到这里,interface{}的本质似乎比较清晰了,看起来就是两个指针变量。到底是不是,我们还要阅读Golang源码。

源码阅读

在第一次了解到Go语言具有反射机制时,就曾猜测它一定保存了表示数据类型的数据结构,否则如何解析类型信息呢?

在反射包中,查看 reflect.TypeOf 函数源码,直接就能找到相关的信息。

图片

原来,interface{}实际上就是:

图片

果然,interface{}就是两个连续存放的指针,与上述分析结果一致。

  • 第一个指针指向数据类型

  • 第二个指针指向数据本身,本例中是123(0x7b)

正因为Golang编译器将源代码中的“数据”包装成类型和数据两部分,并通过指针引用它们,所以,interface{}变量可以引用任何类型的数据,interface{}类型的形式参数可以接收任何类型的实际参数。

实际上,interface{}还等同于Golang源码 internal/reflectlite/value.go 中的emptyInterface结构体。

实际上,interface{}还等同于Golang源码 runtime/runtime2.go 中的eface结构体。

总结

  1. interface{}编译之后就是两个连续存放的指针

    1. 第一个指针指向数据类型

    2. 第二个指针指向数据本身

  2. Golang缓存了[0,256)之间的整数,运行时不再在堆中分配该范围内的整数,有助于减少内存分配和垃圾回收。

    • runtime.staticuint64s 存放在可执行文件的 .noptrdata section

  3. Golang使用栈内存传递参数和返回值,而没有使用寄存器。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Go in memory

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值