一文弄懂:【Go】?jī)?nèi)存中的結(jié)構(gòu)體
結(jié)構(gòu)體
所謂結(jié)構(gòu)體,實(shí)際上就是由各種類型的數(shù)據(jù)組合而成的一種復(fù)合數(shù)據(jù)類型.
在數(shù)據(jù)存儲(chǔ)上來講,結(jié)構(gòu)體和數(shù)組沒有太大的區(qū)別. 只不過結(jié)構(gòu)體的各個(gè)字段(元素)類型可以相同,也可以不同,所以只能通過字段的相對(duì)偏移量進(jìn)行訪問. 而數(shù)組的各個(gè)元素類型相同,可以通過索引快速訪問,實(shí)際其本質(zhì)上也是通過相對(duì)偏移量計(jì)算地址進(jìn)行訪問.
因?yàn)榻Y(jié)構(gòu)體的各個(gè)字段類型不同,有大有小,而結(jié)構(gòu)體在存儲(chǔ)時(shí)通常需要進(jìn)行內(nèi)存對(duì)齊,所以結(jié)構(gòu)體在存儲(chǔ)時(shí)可能會(huì)出現(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個(gè)字節(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個(gè)字節(jié),其結(jié)構(gòu)分布如下:
在之前的幾篇文章中,已經(jīng)詳細(xì)介紹了類型方法相關(guān)內(nèi)容,如果還未閱讀,建議不要錯(cuò)過:
- 再談?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)行時(shí)的寄存器值、內(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兩個(gè)結(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ǎn)單的結(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 = []
這是一個(gè)特殊的結(jié)構(gòu)體,沒有字段,沒有方法,不占用內(nèi)存空間,明明定義在main包中,但是包路徑信息為空,存儲(chǔ)結(jié)構(gòu)分布如下:
好神奇的是,struct{}類型的對(duì)象居然是可以比較的,其比較函數(shù)是runtime.memequal0,定義如下:
- func memequal0(p, q unsafe.Pointer) bool {
- return true
- }
也就是說,所有的struct{}類型的對(duì)象,無論它們?cè)趦?nèi)存的什么位置,無論它們是在什么時(shí)間創(chuàng)建的,永遠(yuǎn)都是相等的.
細(xì)細(xì)品,還是蠻有道理的.
結(jié)構(gòu)體類型flag.FlagSet
結(jié)構(gòu)體flag.FlagSet包含8個(gè)字段,其類型信息占用288個(gè)字節(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ù)對(duì)象,占用96字節(jié)的存儲(chǔ)空間,并且所有字段全部被視為指針數(shù)據(jù).
flag.FlagSet類型的對(duì)象不可比較,因?yàn)槠鋜type.equal字段值nil. 除了struct{}這個(gè)特殊的結(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é)的存儲(chǔ)空間.
bool類型的實(shí)際值只能是0或1,只需要占用一個(gè)字節(jié)即可,實(shí)際的機(jī)器指令也會(huì)讀取一個(gè)字節(jié). 也就是,flag.FlagSet類型的對(duì)象在存儲(chǔ)時(shí),因?yàn)?字節(jié)對(duì)齊,此處需要浪費(fèi)7個(gè)字節(jié)的空間.
從以上字段數(shù)據(jù)可以看到,string類型的字段占16個(gè)字節(jié),[]string類型的字段占24個(gè)字節(jié),接口類型的字段占16個(gè)字節(jié),與之前文章中分析得到的結(jié)果一直.
另外,可以看到map類型的字段,實(shí)際占用8個(gè)字節(jié)的空間,在之后的文章中將會(huì)詳細(xì)介紹map類型.
仔細(xì)的讀者可能已經(jīng)注意到,flag.FlagSet類型沒有任何方法,因?yàn)槠鋟ncommonType.mcount = 0.
在flag/flag.go源文件中,不是定義了很多方法嗎?
以上代碼清單中,flag.FlagSet類型的對(duì)象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類型的對(duì)象f能夠調(diào)用*flag.FlagSet指針類型的方法,只不過是編譯器為方便開發(fā)者實(shí)現(xiàn)的語法糖而已.
在本例中,編譯器會(huì)把flag.FlagSet類型的對(duì)象f的地址作為參數(shù)傳遞給*flag.FlagSet指針類型的方法.反之,編譯器也是支持的.
指針類型*flag.FlagSet
為了方便查看類型信息,筆者開發(fā)了一個(gè)gdb的插件腳本.
查看*flag.FlagSet類型的信息如下,共包含38個(gè)方法,其中34個(gè)是公共方法.此處不再一一介紹.
- (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í)際上,編譯器比想象的做的更多.
有時(shí)候,編譯器會(huì)把源代碼中的一個(gè)方法,編譯出兩個(gè)可執(zhí)行的方法.在 內(nèi)存中的接口類型 一文中,曾進(jìn)行了詳細(xì)分析.
直接運(yùn)行g(shù)db腳本查看reflect.Value類型信息,有3個(gè)字段,75個(gè)方法,此處為方便展示,省略了大部分方法信息.
- (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個(gè)方法.
- (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()方法,編譯之后,生成了兩個(gè)可執(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類型的對(duì)象調(diào)用時(shí),實(shí)際可能執(zhí)行的兩個(gè)方法中的任何一個(gè).
通過*reflect.Value類型的指針對(duì)象調(diào)用時(shí),也可能執(zhí)行的兩個(gè)方法中的任何一個(gè).
這完全是由編譯器決定的.
但是通過接口調(diào)用時(shí),執(zhí)行的一定是reflect.(*Value).Len這個(gè)方法的指令集合.
自定義結(jié)構(gòu)體千變?nèi)f化,但是結(jié)構(gòu)體類型信息相對(duì)還是單一,容易理解.