当前位置:首页 > IT科技类资讯

一文弄懂:【Go】内存中的结构体

结构体

所谓结构体,文弄实际上就是懂G的结由各种类型的数据组合而成的一种复合数据类型.

在数据存储上来讲,结构体和数组没有太大的存中区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,构体所以只能通过字段的文弄相对偏移量进行访问. 而数组的各个元素类型相同,可以通过索引快速访问,懂G的结实际其本质上也是存中通过相对偏移量计算地址进行访问.

因为结构体的各个字段类型不同,有大有小,构体而结构体在存储时通常需要进行内存对齐,文弄所以结构体在存储时可能会出现"空洞",懂G的结也就是存中无法使用到的内存空间.

在之前的Go系列文章中,我们接触最多的构体结构体是reflect包中的rtype,可以说已经非常熟悉.

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位程序和系统中占48个字节,其结构分布如下:

在Go语言中,懂G的结使用reflect.rtype结构体描述任何Go类型的亿华云计算存中基本信息.

在Go语言中,使用reflect.structType结构体描述结构体类别(reflect.Struct)数据的类型信息,定义如下:

// 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位程序和系统中占80个字节,其结构分布如下:

在之前的几篇文章中,已经详细介绍了类型方法相关内容,如果还未阅读,建议不要错过:

再谈整数类型 深入理解函数 内存中的接口类型

在Go语言中,结构体类型不但可以包含字段,还可以定义方法,实际上完整的类型信息结构分布如下:

当然,结构体是可以不包含字段的,也可以没有方法的.

环境

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

声明

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

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

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

代码清单

在Go语言中,香港云服务器结构体随处可见,所以本文示例代码中不再自定义结构体,而是使用Go语言中常用的结构体用于演示.

在 命令行参数详解 一文中,曾详细介绍过flag.FlagSet结构体.

本文,我们将详细介绍flag.FlagSet和reflect.Value两个结构体的类型信息.

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) } 

运行

从运行结果可以看到:

结构体flag.FlagSet的类型信息保存在0x4c2ac0地址处. 结构体指针*flag.FlagSet的类型信息保存在0x4c68e0地址处. 结构体reflect.Value的类型信息保存在0x4ca160地址处. 结构体指针*reflect.Value的类型信息保存在0x4c9c60地址处. 匿名结构体struct{ }{ }的类型信息保存在0x4b4140地址处.

内存分析

在main函数入口处设置断点进行调试.我们先从简单的结构体开始分析.

匿名结构体struct{ }

该结构体既没有字段,也没有方法,其类型信息数据如下:

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 = []

这是一个特殊的结构体,没有字段,没有方法,不占用内存空间,明明定义在main包中,但是包路径信息为空,存储结构分布如下:

好神奇的是,struct{ }类型的对象居然是可以比较的,其比较函数是runtime.memequal0,站群服务器定义如下:

func memequal0(p, q unsafe.Pointer) bool {      return true } 

也就是说,所有的struct{ }类型的对象,无论它们在内存的什么位置,无论它们是在什么时间创建的,永远都是相等的.

细细品,还是蛮有道理的.

结构体类型flag.FlagSet

结构体flag.FlagSet包含8个字段,其类型信息占用288个字节.

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 -> 字段数量 structType.fields.Cap = 8 uncommonType.pkgpath = 0x368 -> "flag" uncommonType.mcount = 0 -> 方法数量 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)   } ] 

从以上数据可以看到,结构体flag.FlagSet类型的数据对象,占用96字节的存储空间,并且所有字段全部被视为指针数据.

flag.FlagSet类型的对象不可比较,因为其rtype.equal字段值nil. 除了struct{ }这个特殊的结构体类型,估计是不容易找到可比较的结构体类型了.

从以上字段数据可以看到,FlagSet.parsed字段的偏移量是24,FlagSet.actual字段的偏移量是32;也就是说,bool类型的FlagSet.parsed字段实际占用8字节的存储空间.

bool类型的实际值只能是0或1,只需要占用一个字节即可,实际的机器指令也会读取一个字节. 也就是,flag.FlagSet类型的对象在存储时,因为8字节对齐,此处需要浪费7个字节的空间.

从以上字段数据可以看到,string类型的字段占16个字节,[]string类型的字段占24个字节,接口类型的字段占16个字节,与之前文章中分析得到的结果一直.

另外,可以看到map类型的字段,实际占用8个字节的空间,在之后的文章中将会详细介绍map类型.

仔细的读者可能已经注意到,flag.FlagSet类型没有任何方法,因为其uncommonType.mcount = 0.

在flag/flag.go源文件中,不是定义了很多方法吗?

以上代码清单中,flag.FlagSet类型的对象f为什么可以调用以下方法呢?

_ = f.Set("hello", "world")   f.PrintDefaults()   fmt.Println(f.Args()) 

实际上,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能够调用*flag.FlagSet指针类型的方法,只不过是编译器为方便开发者实现的语法糖而已.

在本例中,编译器会把flag.FlagSet类型的对象f的地址作为参数传递给*flag.FlagSet指针类型的方法.反之,编译器也是支持的.

指针类型*flag.FlagSet

为了方便查看类型信息,笔者开发了一个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   } ] 

结构体类型reflect.Value

实际上,编译器比想象的做的更多.

有时候,编译器会把源代码中的一个方法,编译出两个可执行的方法.在 内存中的接口类型 一文中,曾进行了详细分析.

直接运行gdb脚本查看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()方法,编译之后,生成了两个可执行方法,分别是:

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类型的对象调用时,实际可能执行的两个方法中的任何一个.

通过*reflect.Value类型的指针对象调用时,也可能执行的两个方法中的任何一个.

这完全是由编译器决定的.

但是通过接口调用时,执行的一定是reflect.(*Value).Len这个方法的指令集合.

自定义结构体千变万化,但是结构体类型信息相对还是单一,容易理解.

分享到:

滇ICP备2023006006号-16