一文弄懂:【Go】內(nèi)存中的結(jié)構(gòu)體
結(jié)構(gòu)體
所謂結(jié)構(gòu)體,實(shí)際上就是由各種類型的數(shù)據(jù)組合而成的一種復(fù)合數(shù)據(jù)類型.
在數(shù)據(jù)存儲上來講,結(jié)構(gòu)體和數(shù)組沒有太大的區(qū)別. 只不過結(jié)構(gòu)體的各個字段(元素)類型可以相同,也可以不同,所以只能通過字段的相對偏移量進(jìn)行訪問. 而數(shù)組的各個元素類型相同,可以通過索引快速訪問,實(shí)際其本質(zhì)上也是通過相對偏移量計(jì)算地址進(jìn)行訪問.
因?yàn)榻Y(jié)構(gòu)體的各個字段類型不同,有大有小,而結(jié)構(gòu)體在存儲時通常需要進(jìn)行內(nèi)存對齊,所以結(jié)構(gòu)體在存儲時可能會出現(xiàn)"空洞",也就是無法使用到的內(nèi)存空間.
在之前的Go系列文章中,我們接觸最多的結(jié)構(gòu)體是reflect包中的rtype,可以說已經(jīng)非常熟悉.
- type rtype struct {
 - size uintptr
 - ptrdata uintptr // number of bytes in the type that can contain pointers
 - hash uint32 // hash of type; avoids computation in hash tables
 - tflag tflag // extra type information flags
 - align uint8 // alignment of variable with this type
 - fieldAlign uint8 // alignment of struct field with this type
 - kind uint8 // enumeration for C
 - equal func(unsafe.Pointer, unsafe.Pointer) bool
 - gcdata *byte // garbage collection data
 - str nameOff // string form
 - ptrToThis typeOff // type for pointer to this type, may be zero
 - }
 
在64位程序和系統(tǒng)中占48個字節(jié),其結(jié)構(gòu)分布如下:
在Go語言中,使用reflect.rtype結(jié)構(gòu)體描述任何Go類型的基本信息.
在Go語言中,使用reflect.structType結(jié)構(gòu)體描述結(jié)構(gòu)體類別(reflect.Struct)數(shù)據(jù)的類型信息,定義如下:
- // structType represents a struct type.
 - type structType struct {
 - rtype
 - pkgPath name
 - fields []structField // sorted by offset
 - }
 - // Struct field
 - type structField struct {
 - name name // name is always non-empty
 - typ *rtype // type of field
 - offsetEmbed uintptr // byte offset of field<<1 | isEmbedded
 - }
 
在64位程序和系統(tǒng)中占80個字節(jié),其結(jié)構(gòu)分布如下:
在之前的幾篇文章中,已經(jīng)詳細(xì)介紹了類型方法相關(guān)內(nèi)容,如果還未閱讀,建議不要錯過:
- 再談?wù)麛?shù)類型
 - 深入理解函數(shù)
 - 內(nèi)存中的接口類型
 
在Go語言中,結(jié)構(gòu)體類型不但可以包含字段,還可以定義方法,實(shí)際上完整的類型信息結(jié)構(gòu)分布如下:
當(dāng)然,結(jié)構(gòu)體是可以不包含字段的,也可以沒有方法的.
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
 - Go : go version go1.16.2 linux/amd64
 
聲明
操作系統(tǒng)、處理器架構(gòu)、Go版本不同,均有可能造成相同的源碼編譯后運(yùn)行時的寄存器值、內(nèi)存地址、數(shù)據(jù)結(jié)構(gòu)等存在差異。
本文僅包含 64 位系統(tǒng)架構(gòu)下的 64 位可執(zhí)行程序的研究分析。
本文僅保證學(xué)習(xí)過程中的分析數(shù)據(jù)在當(dāng)前環(huán)境下的準(zhǔn)確有效性。
代碼清單
在Go語言中,結(jié)構(gòu)體隨處可見,所以本文示例代碼中不再自定義結(jié)構(gòu)體,而是使用Go語言中常用的結(jié)構(gòu)體用于演示.
在 命令行參數(shù)詳解 一文中,曾詳細(xì)介紹過flag.FlagSet結(jié)構(gòu)體.
本文,我們將詳細(xì)介紹flag.FlagSet和reflect.Value兩個結(jié)構(gòu)體的類型信息.
- package main
 - import (
 - "flag"
 - "fmt"
 - "reflect"
 - )
 - func main() {
 - f := flag.FlagSet{}
 - Print(reflect.TypeOf(f))
 - Print(reflect.TypeOf(&f))
 - _ = f.Set("hello", "world")
 - f.PrintDefaults()
 - fmt.Println(f.Args())
 - v := reflect.ValueOf(f)
 - Print(reflect.TypeOf(v))
 - Print(reflect.TypeOf(&v))
 - Print(reflect.TypeOf(struct{}{}))
 - }
 - //go:noinline
 - func Print(t reflect.Type) {
 - fmt.Printf("Type = %s\t, address = %p\n", t, t)
 - }
 
運(yùn)行
從運(yùn)行結(jié)果可以看到:
- 結(jié)構(gòu)體flag.FlagSet的類型信息保存在0x4c2ac0地址處.
 - 結(jié)構(gòu)體指針*flag.FlagSet的類型信息保存在0x4c68e0地址處.
 - 結(jié)構(gòu)體reflect.Value的類型信息保存在0x4ca160地址處.
 - 結(jié)構(gòu)體指針*reflect.Value的類型信息保存在0x4c9c60地址處.
 - 匿名結(jié)構(gòu)體struct{}{}的類型信息保存在0x4b4140地址處.
 
內(nèi)存分析
在main函數(shù)入口處設(shè)置斷點(diǎn)進(jìn)行調(diào)試.我們先從簡單的結(jié)構(gòu)體開始分析.
匿名結(jié)構(gòu)體struct{}
該結(jié)構(gòu)體既沒有字段,也沒有方法,其類型信息數(shù)據(jù)如下:
- rtype.size = 0x0 (0)
 - rtype.ptrdata = 0x0 (0)
 - rtype.hash = 0x27f6ac1b
 - rtype.tflag = tflagExtraStar | tflagRegularMemory
 - rtype.align = 1
 - rtype.fieldAlign = 1
 - rtype.kind = 0x19 (25) -> reflect.Struct
 - rtype.equal = 0x4d3100 -> runtime.memequal0
 - rtype.gcdata = 0x4ea04f
 - rtype.str = 0x0000241f -> "struct {}"
 - rtype.ptrToThis = 0x0 (0x0)
 - structType.pkgPath = 0 -> ""
 - structType.fields = []
 
這是一個特殊的結(jié)構(gòu)體,沒有字段,沒有方法,不占用內(nèi)存空間,明明定義在main包中,但是包路徑信息為空,存儲結(jié)構(gòu)分布如下:
好神奇的是,struct{}類型的對象居然是可以比較的,其比較函數(shù)是runtime.memequal0,定義如下:
- func memequal0(p, q unsafe.Pointer) bool {
 - return true
 - }
 
也就是說,所有的struct{}類型的對象,無論它們在內(nèi)存的什么位置,無論它們是在什么時間創(chuàng)建的,永遠(yuǎn)都是相等的.
細(xì)細(xì)品,還是蠻有道理的.
結(jié)構(gòu)體類型flag.FlagSet
結(jié)構(gòu)體flag.FlagSet包含8個字段,其類型信息占用288個字節(jié).
- rtype.size = 0x60 (96)
 - rtype.ptrdata = 0x60 (96)
 - rtype.hash = 0x644236d1
 - rtype.tflag = tflagUncommon | tflagExtraStar | tflagNamed
 - rtype.align = 8
 - rtype.fieldAlign = 8
 - rtype.kind = 0x19 (25) -> reflect.Struct
 - rtype.equal = nil
 - rtype.gcdata = 0x4e852c
 - rtype.str = 0x32b0 -> "flag.FlagSet"
 - rtype.ptrToThis = 0x208e0 (0x4c68e0)
 - structType.pkgPath = 0x4a6368 -> "flag"
 - structType.fields.Data = 0x4c2b20
 - structType.fields.Len = 8 -> 字段數(shù)量
 - structType.fields.Cap = 8
 - uncommonType.pkgpath = 0x368 -> "flag"
 - uncommonType.mcount = 0 -> 方法數(shù)量
 - uncommonType.xcount = 0
 - uncommonType.moff = 208
 - structType.fields =
 
- [
 - {
 - name = 0x4a69a0 -> Usage
 - typ = 0x4b0140 -> func()
 - offsetEmbed = 0x0 (0)
 - },
 - {
 - name = 0x4a69a0 -> name
 - typ = 0x4b1220 -> string
 - offsetEmbed = 0x8 (8)
 - },
 - {
 - name = 0x4a704a -> parsed
 - typ = 0x4b0460 -> bool
 - offsetEmbed = 0x18 (24)
 - },
 - {
 - name = 0x4a6e64 -> actual
 - typ = 0x4b4c20 -> map[string]*flag.Flag
 - offsetEmbed = 0x20 (32)
 - },
 - {
 - name = 0x4a6f0f -> formal
 - typ = 0x4b4c20 -> map[string]*flag.Flag
 - offsetEmbed = 0x28 (40)
 - },
 - {
 - name = 0x4a646d -> args
 - typ = 0x4afe00 -> []string
 - offsetEmbed = 0x30 (48)
 - },
 - {
 - name = 0x4a9450 -> errorHandling
 - typ = 0x4b05a0 -> flag.ErrorHandling
 - offsetEmbed = 0x48 (72)
 - },
 - {
 - name = 0x4a702f -> output
 - typ = 0x4b65c0 -> io.Writer
 - offsetEmbed = 0x50 (80)
 - }
 - ]
 
從以上數(shù)據(jù)可以看到,結(jié)構(gòu)體flag.FlagSet類型的數(shù)據(jù)對象,占用96字節(jié)的存儲空間,并且所有字段全部被視為指針數(shù)據(jù).
flag.FlagSet類型的對象不可比較,因?yàn)槠鋜type.equal字段值nil. 除了struct{}這個特殊的結(jié)構(gòu)體類型,估計(jì)是不容易找到可比較的結(jié)構(gòu)體類型了.
從以上字段數(shù)據(jù)可以看到,F(xiàn)lagSet.parsed字段的偏移量是24,F(xiàn)lagSet.actual字段的偏移量是32;也就是說,bool類型的FlagSet.parsed字段實(shí)際占用8字節(jié)的存儲空間.
bool類型的實(shí)際值只能是0或1,只需要占用一個字節(jié)即可,實(shí)際的機(jī)器指令也會讀取一個字節(jié). 也就是,flag.FlagSet類型的對象在存儲時,因?yàn)?字節(jié)對齊,此處需要浪費(fèi)7個字節(jié)的空間.
從以上字段數(shù)據(jù)可以看到,string類型的字段占16個字節(jié),[]string類型的字段占24個字節(jié),接口類型的字段占16個字節(jié),與之前文章中分析得到的結(jié)果一直.
另外,可以看到map類型的字段,實(shí)際占用8個字節(jié)的空間,在之后的文章中將會詳細(xì)介紹map類型.
仔細(xì)的讀者可能已經(jīng)注意到,flag.FlagSet類型沒有任何方法,因?yàn)槠鋟ncommonType.mcount = 0.
在flag/flag.go源文件中,不是定義了很多方法嗎?
以上代碼清單中,flag.FlagSet類型的對象f為什么可以調(diào)用以下方法呢?
- _ = f.Set("hello", "world")
 - f.PrintDefaults()
 - fmt.Println(f.Args())
 
實(shí)際上,flag/flag.go源文件中定義的方法的receiver都是*flag.FlagSet指針類型,沒有flag.FlagSet類型.
- // Args returns the non-flag arguments.
 - func (f *FlagSet) Args() []string { return f.args }
 
flag.FlagSet類型的對象f能夠調(diào)用*flag.FlagSet指針類型的方法,只不過是編譯器為方便開發(fā)者實(shí)現(xiàn)的語法糖而已.
在本例中,編譯器會把flag.FlagSet類型的對象f的地址作為參數(shù)傳遞給*flag.FlagSet指針類型的方法.反之,編譯器也是支持的.
指針類型*flag.FlagSet
為了方便查看類型信息,筆者開發(fā)了一個gdb的插件腳本.
查看*flag.FlagSet類型的信息如下,共包含38個方法,其中34個是公共方法.此處不再一一介紹.
- (gdb) info type 0x4c68e0
 - interfaceType {
 - rtype = {
 - size = 0x8 (8)
 - ptrdata = 0x8 (8)
 - hash = 0xe05aa02c
 - tflag = tflagUncommon | tflagRegularMemory
 - align = 8
 - fieldAlign = 8
 - kind = ptr
 - equal = 0x403a00 <runtime.memequal64>
 - gcdata = 0x4d2e28
 - str = *flag.FlagSet
 - ptrToThis = 0x0 (0x0)
 - }
 - elem = 0x4c2ac0 -> flag.FlagSet
 - }
 - uncommonType {
 - pkgpath = flag
 - mcount = 38
 - xcount = 34
 - moff = 16
 - }
 - methods [
 - {
 - name = Arg
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Args
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Bool
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = BoolVar
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Duration
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = DurationVar
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = ErrorHandling
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Float64
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Float64Var
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Func
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Init
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Int
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Int64
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Int64Var
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = IntVar
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Lookup
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = NArg
 - mtyp = 0x4b0960 -> func() int
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = NFlag
 - mtyp = 0x4b0960 -> func() int
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Name
 - mtyp = 0x4b0b20 -> func() string
 - ifn = 0x4a36e0 <flag.(*FlagSet).Name>
 - tfn = 0x4a36e0 <flag.(*FlagSet).Name>
 - },
 - {
 - name = Output
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Parse
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Parsed
 - mtyp = 0x4b0920 -> func() bool
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = PrintDefaults
 - mtyp = 0x4b0140 -> func()
 - ifn = 0x4a3ec0 <flag.(*FlagSet).PrintDefaults>
 - tfn = 0x4a3ec0 <flag.(*FlagSet).PrintDefaults>
 - },
 - {
 - name = Set
 - mtyp = nil
 - ifn = 0x4a37a0 <flag.(*FlagSet).Set>
 - tfn = 0x4a37a0 <flag.(*FlagSet).Set>
 - },
 - {
 - name = SetOutput
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = String
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = StringVar
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Uint
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Uint64
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Uint64Var
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = UintVar
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Var
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Visit
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = VisitAll
 - mtyp = nil
 - ifn = 0x4a3700 <flag.(*FlagSet).VisitAll>
 - tfn = 0x4a3700 <flag.(*FlagSet).VisitAll>
 - },
 - {
 - name = defaultUsage
 - mtyp = 0x4b0140 -> func()
 - ifn = 0x4a3f20 <flag.(*FlagSet).defaultUsage>
 - tfn = 0x4a3f20 <flag.(*FlagSet).defaultUsage>
 - },
 - {
 - name = failf
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = parseOne
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = usage
 - mtyp = 0x4b0140 -> func()
 - ifn = nil
 - tfn = nil
 - }
 - ]
 
結(jié)構(gòu)體類型reflect.Value
實(shí)際上,編譯器比想象的做的更多.
有時候,編譯器會把源代碼中的一個方法,編譯出兩個可執(zhí)行的方法.在 內(nèi)存中的接口類型 一文中,曾進(jìn)行了詳細(xì)分析.
直接運(yùn)行g(shù)db腳本查看reflect.Value類型信息,有3個字段,75個方法,此處為方便展示,省略了大部分方法信息.
- (gdb) info type 0x4ca160
 - structType {
 - rtype = {
 - size = 0x18 (24)
 - ptrdata = 0x10 (16)
 - hash = 0x500c1abc
 - tflag = tflagUncommon | tflagExtraStar | tflagNamed | tflagRegularMemory
 - align = 8
 - fieldAlign = 8
 - kind = struct
 - equal = 0x402720 <runtime.memequal_varlen>
 - gcdata = 0x4d2e48
 - str = reflect.Value
 - ptrToThis = 0x23c60 (0x4c9c60)
 - }
 - pkgPath = reflect
 - fields = [
 - {
 - name = 0x4875094 -> typ
 - typ = 0x4c6e60 -> *reflect.rtype
 - offsetEmbed = 0x0 (0)
 - },
 - {
 - name = 0x4874896 -> ptr
 - typ = 0x4b13e0 -> unsafe.Pointer
 - offsetEmbed = 0x8 (8)
 - },
 - {
 - name = 0x4875112 -> flag
 - typ = 0x4be7c0 -> reflect.flag
 - offsetEmbed = 0x10 (16) embed
 - }
 - ]
 - }
 - uncommonType {
 - pkgpath = reflect
 - mcount = 75
 - xcount = 61
 - moff = 88
 - }
 - methods [
 - {
 - name = Addr
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Bool
 - mtyp = 0x4b0920 -> func() bool
 - ifn = nil
 - tfn = 0x4881c0 <reflect.Value.Bool>
 - },
 - ......
 - {
 - name = Kind
 - mtyp = 0x4b0aa0 -> func() reflect.Kind
 - ifn = 0x48d500 <reflect.(*Value).Kind>
 - tfn = 0x489400 <reflect.Value.Kind>
 - },
 - {
 - name = Len
 - mtyp = 0x4b0960 -> func() int
 - ifn = 0x48d560 <reflect.(*Value).Len>
 - tfn = 0x489420 <reflect.Value.Len>
 - },
 - ......
 - ]
 
再看*reflect.Value指針類型的信息,沒有任何字段(畢竟是指針),也有75個方法.
- (gdb) info type 0x4c9c60
 - interfaceType {
 - rtype = {
 - size = 0x8 (8)
 - ptrdata = 0x8 (8)
 - hash = 0xf764ad0
 - tflag = tflagUncommon | tflagRegularMemory
 - align = 8
 - fieldAlign = 8
 - kind = ptr
 - equal = 0x403a00 <runtime.memequal64>
 - gcdata = 0x4d2e28
 - str = *reflect.Value
 - ptrToThis = 0x0 (0x0)
 - }
 - elem = 0x4ca160 -> reflect.Value
 - }
 - uncommonType {
 - pkgpath = reflect
 - mcount = 75
 - xcount = 61
 - moff = 16
 - }
 - methods [
 - {
 - name = Addr
 - mtyp = nil
 - ifn = nil
 - tfn = nil
 - },
 - {
 - name = Bool
 - mtyp = 0x4b0920 -> func() bool
 - ifn = nil
 - tfn = nil
 - },
 - ......
 - {
 - name = Kind
 - mtyp = 0x4b0aa0 -> func() reflect.Kind
 - ifn = 0x48d500 <reflect.(*Value).Kind>
 - tfn = 0x48d500 <reflect.(*Value).Kind>
 - },
 - {
 - name = Len
 - mtyp = 0x4b0960 -> func() int
 - ifn = 0x48d560 <reflect.(*Value).Len>
 - tfn = 0x48d560 <reflect.(*Value).Len>
 - },
 - ......
 - ]
 
我們可以清楚地看到,在源碼中Len()方法,編譯之后,生成了兩個可執(zhí)行方法,分別是:
- reflect.Value.Len
 - reflect.(*Value).Len
 
- func (v Value) Len() int {
 - k := v.kind()
 - switch k {
 - case Array:
 - tt := (*arrayType)(unsafe.Pointer(v.typ))
 - return int(tt.len)
 - case Chan:
 - return chanlen(v.pointer())
 - case Map:
 - return maplen(v.pointer())
 - case Slice:
 - // Slice is bigger than a word; assume flagIndir.
 - return (*unsafeheader.Slice)(v.ptr).Len
 - case String:
 - // String is bigger than a word; assume flagIndir.
 - return (*unsafeheader.String)(v.ptr).Len
 - }
 - panic(&ValueError{"reflect.Value.Len", v.kind()})
 - }
 
通過reflect.Value類型的對象調(diào)用時,實(shí)際可能執(zhí)行的兩個方法中的任何一個.
通過*reflect.Value類型的指針對象調(diào)用時,也可能執(zhí)行的兩個方法中的任何一個.
這完全是由編譯器決定的.
但是通過接口調(diào)用時,執(zhí)行的一定是reflect.(*Value).Len這個方法的指令集合.
自定義結(jié)構(gòu)體千變?nèi)f化,但是結(jié)構(gòu)體類型信息相對還是單一,容易理解.






















 
 
 
















 
 
 
 