Go语言之再谈整数类型

开发 后端
在Go语言中,type关键字不仅可以定义结构体(struct)和接口(interface),实际上可以用于声明任何数据类型,非常非常地强悍。

[[427102]]

 前言

【Go】内存中的整数 一文详细介绍了int类型,对 int 数据及其类型建立起基本的认识。

再谈整数类型的目的,是为了进一步剖析Go语言的类型系统,从底层化解潜在的错误认知。

在Go语言中,type关键字不仅可以定义结构体(struct)和接口(interface),实际上可以用于声明任何数据类型,非常非常地强悍。例如,

  1. type calc func(a, b intint 
  2.  
  3. type Foo int 

有人说,在以上代码中,type关键字的作用是定义类型的别名,Foo就是int的别名,Foo类型就是int类型。

本文将带你深入了解int类型与Foo类型,保证你吃不了亏,保证你上不了当。

环境

  1. OS : Ubuntu 20.04.2 LTS; x86_64  
  2. Go : go version go1.16.2 linux/amd64 

声明

操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。

本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析。

本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

代码清单

int_kind.go

  1. package main 
  2.  
  3. import "fmt" 
  4. import "reflect" 
  5. import "strconv" 
  6.  
  7. type Foo int 
  8.  
  9. //go:noinline 
  10. func (f Foo) Ree() int { 
  11.   return int(f) 
  12.  
  13. //go:noinline 
  14. func (f Foo) String() string { 
  15.   return strconv.Itoa(f.Ree()) 
  16.  
  17. //go:noinline 
  18. func (f Foo) print() { 
  19.   fmt.Println("foo is " + f.String()) 
  20.  
  21. func main() { 
  22.   Typeof(123) 
  23.   Typeof(Foo(456)) 
  24.  
  25. //go:noinline 
  26. func Typeof(i interface{}) { 
  27.   t := reflect.TypeOf(i) 
  28.   fmt.Println("值  ", i) 
  29.   fmt.Println("名称", t.Name()) 
  30.   fmt.Println("类型", t.String()) 
  31.   fmt.Println("方法"
  32.   num := t.NumMethod() 
  33.   if num > 0 { 
  34.     for j := 0; j < num; j++ { 
  35.       fmt.Println("  ", t.Method(j).Name, t.Method(j).Type) 
  36.     } 
  37.   } 
  38.   fmt.Println() 

代码清单中,Typeof函数用于显示数据对象的类型信息。

运行结果

仅仅从运行结果看,我们就知道Foo类型不是int类型,Foo不是int的别名。

数据结构介绍

在reflect/type.go源文件中,定义了两个数据结构uncommonType和method,用于存储和解析数据类型的方法信息。

  1. type uncommonType struct { 
  2.     pkgPath nameOff  // 包路径名称偏移量 
  3.     mcount  uint16   // 方法的数量 
  4.     xcount  uint16   // 公共导出方法的数量 
  5.     moff    uint32   // [mcount]method 相对本对象起始地址的偏移量 
  6.     _       uint32   // unused 

reflect.uncommonType结构体用于描述一个数据类型的包名和方法信息。

  1. // 非接口类型的方法 
  2. type method struct { 
  3.     name nameOff // 方法名称偏移量 
  4.     mtyp typeOff // 方法类型偏移量 
  5.     ifn  textOff // 通过接口调用时的地址偏移量;接口类型本文不介绍 
  6.     tfn  textOff // 直接类型调用时的地址偏移量 

reflect.method结构体用于描述一个方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量。

  1. type nameOff int32 // offset to a name 
  2. type typeOff int32 // offset to an *rtype 
  3. 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类型信息结构如下伪代码所示:

  1. type intType struct { 
  2.   rtype 
  3.   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类型复杂许多,其类型信息结构如下伪代码所示:

  1. type FooType struct { 
  2.   rtype 
  3.   u uncommonType 
  4.   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源文件中的代码印证这一点:

  1. func (t *uncommonType) methods() []method { 
  2.   if t.mcount == 0 { 
  3.     return nil 
  4.   } 
  5.   return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount] 
  6.  
  7. func (t *uncommonType) exportedMethods() []method { 
  8.   if t.xcount == 0 { 
  9.     return nil 
  10.   } 
  11.   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 语言的类型系统有了更加深入的了解,和想象中的是否有所不同?

责任编辑:武晓燕 来源: Golang In Memory
相关推荐

2021-10-23 06:42:14

Go语言接口

2020-12-31 09:06:44

Go语言Reflect

2021-10-16 17:53:35

Go函数编程

2021-10-09 07:52:01

Go程序重命名

2022-03-28 13:34:26

Go泛型部署泛型

2019-01-03 09:45:20

Go 前端 Web

2024-01-05 20:46:14

2013-08-20 10:11:20

Go系统管理员

2021-05-12 08:53:54

Go语言调度

2021-10-18 10:53:26

Go 代码技术

2023-06-26 00:03:55

Go语言类型

2023-07-16 23:43:05

Go语言模式

2024-01-08 08:23:07

Go语言代码

2013-07-10 11:11:05

PythonGo语言

2012-08-13 14:13:46

2018-08-01 15:10:02

GolangPython语言

2012-02-13 10:03:31

编程开发

2017-06-14 09:37:05

R语言Apriori算法

2011-07-29 10:12:12

JavaScript

2012-12-11 09:16:07

Go
点赞
收藏

51CTO技术栈公众号