Go語言之再談?wù)麛?shù)類型
前言
【Go】內(nèi)存中的整數(shù) 一文詳細介紹了int類型,對 int 數(shù)據(jù)及其類型建立起基本的認識。
再談?wù)麛?shù)類型的目的,是為了進一步剖析Go語言的類型系統(tǒng),從底層化解潛在的錯誤認知。
在Go語言中,type關(guān)鍵字不僅可以定義結(jié)構(gòu)體(struct)和接口(interface),實際上可以用于聲明任何數(shù)據(jù)類型,非常非常地強悍。例如,
- type calc func(a, b int) int
- type Foo int
有人說,在以上代碼中,type關(guān)鍵字的作用是定義類型的別名,F(xiàn)oo就是int的別名,F(xiàn)oo類型就是int類型。
本文將帶你深入了解int類型與Foo類型,保證你吃不了虧,保證你上不了當(dāng)。
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
聲明
操作系統(tǒng)、處理器架構(gòu)、Go版本不同,均有可能造成相同的源碼編譯后運行時的寄存器值、內(nèi)存地址、數(shù)據(jù)結(jié)構(gòu)等存在差異。
本文僅包含 64 位系統(tǒng)架構(gòu)下的 64 位可執(zhí)行程序的研究分析。
本文僅保證學(xué)習(xí)過程中的分析數(shù)據(jù)在當(dāng)前環(huán)境下的準(zhǔn)確有效性。
代碼清單
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函數(shù)用于顯示數(shù)據(jù)對象的類型信息。
運行結(jié)果
僅僅從運行結(jié)果看,我們就知道Foo類型不是int類型,F(xiàn)oo不是int的別名。
數(shù)據(jù)結(jié)構(gòu)介紹
在reflect/type.go源文件中,定義了兩個數(shù)據(jù)結(jié)構(gòu)uncommonType和method,用于存儲和解析數(shù)據(jù)類型的方法信息。
- type uncommonType struct {
- pkgPath nameOff // 包路徑名稱偏移量
- mcount uint16 // 方法的數(shù)量
- xcount uint16 // 公共導(dǎo)出方法的數(shù)量
- moff uint32 // [mcount]method 相對本對象起始地址的偏移量
- _ uint32 // unused
- }
reflect.uncommonType結(jié)構(gòu)體用于描述一個數(shù)據(jù)類型的包名和方法信息。
- // 非接口類型的方法
- type method struct {
- name nameOff // 方法名稱偏移量
- mtyp typeOff // 方法類型偏移量
- ifn textOff // 通過接口調(diào)用時的地址偏移量;接口類型本文不介紹
- tfn textOff // 直接類型調(diào)用時的地址偏移量
- }
reflect.method結(jié)構(gòu)體用于描述一個方法,它是一個壓縮格式的結(jié)構(gòu),每個字段的值都是一個相對偏移量。
- 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 節(jié)起始地址的偏移量。
- typeOff 是相對程序 .rodata 節(jié)起始地址的偏移量。
- textOff 是相對程序 .text 節(jié)起始地址的偏移量。
- 關(guān)于 reflect.name結(jié)構(gòu)體的介紹,請閱讀 【Go】內(nèi)存中的整數(shù) 。
內(nèi)存分析
在Typeof函數(shù)入口處設(shè)置斷點,首先查看 123 這個 int 對象的類型信息。
int 類型
在 【Go】內(nèi)存中的整數(shù) 一文,介紹了int類型信息占用 48 個字節(jié), 實際上int類型信息占用 64 個字節(jié),只不過int類型并沒有任何方法(method),所以前文忽略了uncommonType數(shù)據(jù)。
int類型信息結(jié)構(gòu)如下偽代碼所示:
- type intType struct {
- rtype
- u uncommonType
- }
其結(jié)構(gòu)分布如下圖所示:
本文要更進一步分析數(shù)據(jù)的類型,所以需要將uncommonType數(shù)據(jù)拿出來對比。
- 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類型數(shù)據(jù)繪制成圖表如下:
此處不再對int類型信息進行詳細介紹,僅說明 rtype.tflag字段;該字段包含reflect.tflagUncommon標(biāo)記,表示類型信息中包含uncommonType數(shù)據(jù)。
uncommonType.mcount = 0表示類型信息中不包含方法信息。
Foo 類型
Foo類型因為包含方法信息,要比int類型復(fù)雜許多,其類型信息結(jié)構(gòu)如下偽代碼所示:
- type FooType struct {
- rtype
- u uncommonType
- methods [u.mcount]method
- }
結(jié)構(gòu)分布如下圖所示:
以同樣的方式查看Foo類型數(shù)據(jù):
- 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 -> 方法數(shù)量
- uncommonType.xcount = 2 -> 公共導(dǎo)出方法數(shù)量
- 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類型數(shù)據(jù)繪制成圖表如下:
類型對比
- int和Foo兩種類型屬于同一種數(shù)據(jù)類別(reflect.Kind),都是reflect.Int。
- int和Foo兩種類型比較函數(shù)相同,都是runtime.memequal64。
- int和Foo數(shù)據(jù)對象內(nèi)存大小相同,都是8。
- int和Foo數(shù)據(jù)對象內(nèi)存對齊相同,都是8。
- int和Foo兩種類型名稱不同。
- int和Foo兩種類型哈希種子不同。
- int和Foo兩種類型方法數(shù)量不同。
- int和Foo兩種類型的指針類型不同。
類型方法
我們再回顧一下reflect.method結(jié)構(gòu)體的各個字段:
- name字段描述的是方法名稱偏移量。
- mtyp字段描述的是方法類型信息偏移量;關(guān)于函數(shù)類型介紹,敬請期待。
- ifn字段描述的是接口調(diào)用該方法時的指令內(nèi)存地址偏移量;關(guān)于接口類型介紹,敬請期待。
- tfn字段描述的是直接調(diào)用該方法時的指令內(nèi)存地址偏移量。
Foo類型有3個方法,它們的類型信息保存在0x4dd8e0地址處;通過偏移量計算地址,查看方法的名稱、地址、指令。
方法名稱
- methods[0].name = Ree
- methods[1].name = String
- methods[2].name = print
從內(nèi)存分析數(shù)據(jù)看,F(xiàn)oo類型的三個方法信息的保存順序似乎與源碼中定義的順序相同,其實不然。
數(shù)據(jù)類型的方法信息保存順序是大寫字母開頭的公共導(dǎo)出方法在前,小寫字母開頭的包私有方法在后,我們可以通過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]
- }
方法類型
關(guān)于函數(shù)類型與接口方法,后續(xù)會有專題文章詳細介紹,本文將不再深入探究。
方法地址
從內(nèi)存數(shù)據(jù)看到,
- Ree方法的地址偏移是0x000c6fe0,通過計算可以在0x4c7fe0地址處找到其機器指令。
- String方法的地址偏移是0x000c7000,通過計算可以在0x4c8000地址處找到其機器指令。
- print方法的地址偏移是0xffffffff,也就是-1,意思是找不到該方法。
我們明明在源碼中定義了print方法,為什么找不到該方法呢?
原因是:print方法是一個私有方法,不會被外部調(diào)用,但是main包范圍內(nèi)又沒有調(diào)用者; Go編譯器本著勤儉節(jié)約的原則,把print方法優(yōu)化丟棄掉了,即使使用go:noinline指令禁止內(nèi)斂也不管用,就是直接干掉。
Go編譯器的類似優(yōu)化行為隨處可見,在后續(xù)文章中會逐步介紹。
通過本文,詳細你對 type 關(guān)鍵字有了更加深入的了解,對 Go 語言的類型系統(tǒng)有了更加深入的了解,和想象中的是否有所不同?