Go語言之再談整數類型
前言
【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 語言的類型系統有了更加深入的了解,和想象中的是否有所不同?