前言
【Go】内存中的整数 一文详细介绍了int类型,对 int 数据及其类型建立起基本的认识。
再谈整数类型的目的,是为了进一步剖析Go语言的类型系统,从底层化解潜在的错误认知。
在Go语言中,type关键字不仅可以定义结构体(struct)和接口(interface),实际上可以用于声明任何数据类型,非常非常地强悍。例如,
- type calc func(a, b int) int
- type Foo int
有人说,在以上代码中,type关键字的作用是定义类型的别名,Foo就是int的别名,Foo类型就是int类型。
本文将带你深入了解int类型与Foo类型,保证你吃不了亏,保证你上不了当。
环境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
声明
操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。
本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析。
本文仅保证学习过程中的分析数据在当前环境下的准确有效性。
代码清单
int_kind.go
- package main
- import "fmt"
- import "reflect"
- import "strconv"
- type Foo int
- //go:noinline
- func (f Foo) Ree() int {
- return int(f)
- }
- //go:noinline
- func (f Foo) String() string {
- return strconv.Itoa(f.Ree())
- }
- //go:noinline
- func (f Foo) print() {
- fmt.Println("foo is " + f.String())
- }
- func main() {
- Typeof(123)
- Typeof(Foo(456))
- }
- //go:noinline
- func Typeof(i interface{}) {
- t := reflect.TypeOf(i)
- fmt.Println("值 ", i)
- fmt.Println("名称", t.Name())
- fmt.Println("类型", t.String())
- fmt.Println("方法")
- num := t.NumMethod()
- if num > 0 {
- for j := 0; j < num; j++ {
- fmt.Println(" ", t.Method(j).Name, t.Method(j).Type)
- }
- }
- fmt.Println()
- }
代码清单中,Typeof函数用于显示数据对象的类型信息。
运行结果
仅仅从运行结果看,我们就知道Foo类型不是int类型,Foo不是int的别名。
数据结构介绍
在reflect/type.go源文件中,定义了两个数据结构uncommonType和method,用于存储和解析数据类型的方法信息。
- type uncommonType struct {
- pkgPath nameOff // 包路径名称偏移量
- mcount uint16 // 方法的数量
- xcount uint16 // 公共导出方法的数量
- moff uint32 // [mcount]method 相对本对象起始地址的偏移量
- _ uint32 // unused
- }
reflect.uncommonType结构体用于描述一个数据类型的包名和方法信息。
- // 非接口类型的方法
- type method struct {
- name nameOff // 方法名称偏移量
- mtyp typeOff // 方法类型偏移量
- ifn textOff // 通过接口调用时的地址偏移量;接口类型本文不介绍
- tfn textOff // 直接类型调用时的地址偏移量
- }
reflect.method结构体用于描述一个方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量。
- type nameOff int32 // offset to a name
- type typeOff int32 // offset to an *rtype
- type textOff int32 // offset from top of text section
- nameOff 是相对程序 .rodata 节起始地址的偏移量。
- typeOff 是相对程序 .rodata 节起始地址的偏移量。
- textOff 是相对程序 .text 节起始地址的偏移量。
- 关于 reflect.name结构体的介绍,请阅读 【Go】内存中的整数 。
内存分析
在Typeof函数入口处设置断点,首先查看 123 这个 int 对象的类型信息。
int 类型
在 【Go】内存中的整数 一文,介绍了int类型信息占用 48 个字节, 实际上int类型信息占用 64 个字节,只不过int类型并没有任何方法(method),所以前文忽略了uncommonType数据。
int类型信息结构如下伪代码所示:
- type intType struct {
- rtype
- u uncommonType
- }
其结构分布如下图所示:
本文要更进一步分析数据的类型,所以需要将uncommonType数据拿出来对比。
- rtype.size = 8
- rtype.ptrdata = 0
- rtype.hash = 0xf75371fa
- rtype.tflag = 0xf = reflect.tflagUncommon | reflect.tflagExtraStar | reflect.tflagNamed | reflect.tflagRegularMemory
- rtype.align = 8
- rtype.fieldAlign = 8
- rtype.kind = 2 = reflect.Int
- rtype.equal = 0x4fbd98 -> runtime.memequal64
- rtype.str = 0x000003e3 -> *int字符串
- rtype.ptrToThis = 0x00007c00 -> *int类型
- uncommonType.pkgPath = 0
- uncommonType.mcount = 0 -> 没有方法
- uncommonType.xcount = 0
- uncommonType.moff = 0x10
将int类型数据绘制成图表如下:
此处不再对int类型信息进行详细介绍,仅说明 rtype.tflag字段;该字段包含reflect.tflagUncommon标记,表示类型信息中包含uncommonType数据。
uncommonType.mcount = 0表示类型信息中不包含方法信息。
Foo 类型
Foo类型因为包含方法信息,要比int类型复杂许多,其类型信息结构如下伪代码所示:
- type FooType struct {
- rtype
- u uncommonType
- methods [u.mcount]method
- }
结构分布如下图所示:
以同样的方式查看Foo类型数据:
- rtype.size = 8
- rtype.ptrdata = 0
- rtype.hash = 0xec552021
- rtype.tflag = 0xf = reflect.tflagUncommon | reflect.tflagExtraStar | reflect.tflagNamed | reflect.tflagRegularMemory
- rtype.align = 8
- rtype.fieldAlign = 8
- rtype.kind = 2 = reflect.Int
- rtype.equal = 0x4fbd98 -> runtime.memequal64
- rtype.str = 0x00002128 -> *main.Foo字符串
- rtype.ptrToThis = 0x00014c00 -> *Foo类型
- uncommonType.pkgPath = 0x000003c4 -> main字符串
- uncommonType.mcount = 3 -> 方法数量
- uncommonType.xcount = 2 -> 公共导出方法数量
- uncommonType.moff = 0x10
- method[0].name = 0x000001e8
- method[0].mtyp = 0x0000be60
- method[0].ifn = 0x000c7740
- method[0].tfn = 0x000c6fe0
- method[1].name = 0x00001025
- method[1].mtyp = 0x0000c0e0
- method[1].ifn = 0x000c77c0
- method[1].tfn = 0x000c7000
- method[2].name = 0x00000da0
- method[2].mtyp = 0x0000b600
- method[2].ifn = 0xffffffff
- method[2].tfn = 0xffffffff
将Foo类型数据绘制成图表如下:
类型对比
- int和Foo两种类型属于同一种数据类别(reflect.Kind),都是reflect.Int。
- int和Foo两种类型比较函数相同,都是runtime.memequal64。
- int和Foo数据对象内存大小相同,都是8。
- int和Foo数据对象内存对齐相同,都是8。
- int和Foo两种类型名称不同。
- int和Foo两种类型哈希种子不同。
- int和Foo两种类型方法数量不同。
- int和Foo两种类型的指针类型不同。
类型方法
我们再回顾一下reflect.method结构体的各个字段:
- name字段描述的是方法名称偏移量。
- mtyp字段描述的是方法类型信息偏移量;关于函数类型介绍,敬请期待。
- ifn字段描述的是接口调用该方法时的指令内存地址偏移量;关于接口类型介绍,敬请期待。
- tfn字段描述的是直接调用该方法时的指令内存地址偏移量。
Foo类型有3个方法,它们的类型信息保存在0x4dd8e0地址处;通过偏移量计算地址,查看方法的名称、地址、指令。
方法名称
- methods[0].name = Ree
- methods[1].name = String
- methods[2].name = print
从内存分析数据看,Foo类型的三个方法信息的保存顺序似乎与源码中定义的顺序相同,其实不然。
数据类型的方法信息保存顺序是大写字母开头的公共导出方法在前,小写字母开头的包私有方法在后,我们可以通过reflect/type.go源文件中的代码印证这一点:
- func (t *uncommonType) methods() []method {
- if t.mcount == 0 {
- return nil
- }
- return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount]
- }
- func (t *uncommonType) exportedMethods() []method {
- if t.xcount == 0 {
- return nil
- }
- return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount]
- }
方法类型
关于函数类型与接口方法,后续会有专题文章详细介绍,本文将不再深入探究。
方法地址
从内存数据看到,
- Ree方法的地址偏移是0x000c6fe0,通过计算可以在0x4c7fe0地址处找到其机器指令。
- String方法的地址偏移是0x000c7000,通过计算可以在0x4c8000地址处找到其机器指令。
- print方法的地址偏移是0xffffffff,也就是-1,意思是找不到该方法。
我们明明在源码中定义了print方法,为什么找不到该方法呢?
原因是:print方法是一个私有方法,不会被外部调用,但是main包范围内又没有调用者; Go编译器本着勤俭节约的原则,把print方法优化丢弃掉了,即使使用go:noinline指令禁止内敛也不管用,就是直接干掉。
Go编译器的类似优化行为随处可见,在后续文章中会逐步介绍。
通过本文,详细你对 type 关键字有了更加深入的了解,对 Go 语言的类型系统有了更加深入的了解,和想象中的是否有所不同?