上一篇 内存中的空接口 研究了 int 类型数据转变为 interface{} 类型数据的过程。
本文主要研究 int 类型 在内存中真实的样子。
环境
OS : Ubuntu 20.04.2 LTS; x86_64
Go : go version go1.16.2 linux/amd64
约定
-
Go中描述数据类型的数据结构(reflect.rtype)称为Go类型结构(体)。
-
Go中指向描述数据类型的数据结构(reflect.rtype)的指针,称为Go类型指针。
源码阅读
在 内存中的空接口 中,我们知道了 interface{} 类型变量实际包含两个字段(指针变量)。
其中,第一个字段(typ)是类型指针,第二个字段(word)是数据指针。
Go类型结构体
在Golang源代码中,有三处定义了Go类型结构体:
-
reflect/type.go中的rtype结构体
-
internal/reflectlite/type.go中的rtype结构体
-
runtime/type.go中的_type结构体
它们虽然是不同的类型,但是它们是等价的,在内存中是可以互换的、相互引用的。
我们来看reflect/type.go中的rtype结构体:
在64位程序和操作系统中,其各个字段的分布结构如下:
代码清单
仍然使用 内存中的空接口 中的代码。
package main
import "fmt"
func main() {
var a = 123
PrintInterface(a)
}
//go:noinline
func PrintInterface(v interface{}) {
fmt.Println("it =", v)
}
深入内存
动态调试
编译以上代码,使用gdb动态调试,在调用PrintInterface函数的指令处设置断点,查看 int 类型的类型数据(48个字节)。
上图中,小红框标记的就是栈顶保存的指针值(typ),大红框标记的就是reflect.rtype结构体在内存中表示 int 类型的数据对象。
字段详解
备注:Intel系列芯片采用小端模式。所以查看内存时注意字节顺序。
rtype.size
地址 0x4a2140 处的8个字节存储了size的值。
int 类型数据在内存中占8个字节长度。
rtype.ptrdata
地址 0x4a2148 处的8个字节存储了ptrdata的值。
int 类型数据不包含指针,所以值为0。
rtype.hash
地址 0x4a2150 处的4个字节存储了类型的hash值。
类型的哈希值,用于哈希计算。
Ubuntu操作系统和Windows操作系统,编译的程序,int 类型 hash 相同。
Go编译器具体如何生成hash值尚未研究。
rtype.tflag
地址 0x4a2154 处的1个字节存储了额外的类型信息标记,值为0xF(0b00001111)。
在Go语言源代码中,定义了4个标记常量,用于位与运算:
const tflagUncommon = 1 << 0 // 0b00000001
const tflagExtraStar = 1 << 1 // 0b00000010
const tflagNamed = 1 << 2 // 0b00000100
const tflagRegularMemory= 1 << 3 // 0b00001000
-
tflagUncommon表示该rtype对象后是否紧跟着额外的包导入、方法信息。
-
tflagExtraStar表示rtype.str字段指向的字符串是否包含"*"前缀。稍后将会详细介绍。
-
tflagNamed表示该数据类型是否有名称。
-
tflagRegularMemory means that equal and hash functions can treat this type as a single region of t.size bytes.。
很明显,int 类型符合以下特征
-
包含uncommonType信息(所以实际上 int 类型结构体包含64个字节)
-
有类型名字
-
rtype.str字段有*前缀
-
数据连续存储
rtype.align
int 类型数据在内存中8字节对齐。
rtype.fieldAlign
int 类型数据作为结构体字段时,在内存中8字节对齐。
rtype.kind
Golang对数据类型(Type)进行了分类(Kind),一共有26种。
int 类型的分类Int,值是2。
rtype.equal
地址 0x4a2158 处的8个字节存储了用于该类型数据 equal 比较的函数指针。
从图上可以看出,int 类型数据使用runtime.memequal64函数进行相等比较。
rtype.gcdata
垃圾收集数据。
rtype.str
地址 0x4a2168 处的4个字节存储了该类型的文字描述信息(string form)。实际上,这4个字节保存的只是该类型文字描述信息的相对偏移量。int 类型的文字描述信息偏移量是0x34f。但,它是相对哪个值进行偏移的?
通过查看源代码和猜测推断,很快定位到,偏移量的基址是.rodata section的起始地址。
Golang源代码地址:reflect.resolveNameOff,runtime.resolveNameOff
0x0000000000498000 + 0x34f = 0x49834F
地址 0x49834F 指向的是一个结构体数据,其定义如下,注释非常清晰的解释了其结构组成。
// name is an encoded type name with optional extra data.
//
// The first byte is a bit field containing:
//
// 1<<0 the name is exported
// 1<<1 tag data follows the name
// 1<<2 pkgPath nameOff follows the name and tag
//
// The next two bytes are the data length:
//
// l := uint16(data[1])<<8 | uint16(data[2])
//
// Bytes [3:3+l] are the string data.
//
// If tag data follows then bytes 3+l and 3+l+1 are the tag length,
// with the data following.
//
// If the import path follows, then 4 bytes at the end of
// the data form a nameOff. The import path is only set for concrete
// methods that are defined in a different package than their type.
//
// If a name starts with "*", then the exported bit represents
// whether the pointed to type is exported.
type name struct {
bytes *byte
}
把注释画成图,就非常清晰了:
虚线框选的部分,可能存在,也可能不存在,具体存不存在由第一个字节 flag 决定。
通过查看内存,int 类型的文字描述(string form)由4个字符组成:*int
int 类型的名称就是从这个字符串解析出来的。因为 int 类型的 rtype.tflag 包含 tflagExtraStar 标记,所以在获取类型名称的时候会把 * 号去掉,最终是 int。
rtype.ptrToThis
地址 0x4a216c 处的4个字节存储了该字段的值。
它也是一个偏移量,值可能为0。
表示该类型的指针类型结构体存储位置偏移量(令人读过之后一脸懵逼)。
以int类型为例,ptrToThis是 *int 类型结构体相对.rodata section起始地址的偏移量。
具体数据含义不再赘述,可按照本文的分析过程再来分析一次即可。
总结发现
-
Golang中描述数据类型的数据结构包含48个字节,了解了各个字段的含义。
-
int 类型数据占8个字节。
-
*int 类型数据也占8个字节。
-
int 类型文字描述信息(string form)为 *int,类型名称为 int。
-
*int 类型文字描述信息(string form)为 *int,没有类型名称。
-
int 类型和 *int 类型具有相同的 equal 函数。