所谓结构,其实就是各种数据类型组成的复合数据类型。在数据存储方面,结构和数组没有太大区别。只是结构体的各个字段(元素)的类型可以相同也可以不同,所以只能通过字段的相对偏移来访问。数组的元素类型相同,可以通过索引快速访问,其实本质上是通过相对偏移量计算地址来访问的。因为结构体的各个字段的类型不同,有大有小,而且结构体在存储的时候通常需要内存对齐,所以结构体存储的时候可能会有一个“洞”,也就是存储的内存空间不能使用。在之前的Go系列文章中,我们接触最多的结构就是reflect包中的rtype,可以说是非常熟悉了。typertypestruct{sizeuintptrptrdatauintptr//numberofbytesinthetypethatcancontainpointershashuint32//hashoftype;avoidscomputationinhashtablestflagtflag//extratypeinformationflagsalignuint8//alignmentofvariablewiththistypefieldAlignuint8//alignmentofstructfieldwiththistypekinduint8//enumerationforCequalfunc(unsafe.Pointer,unsafe.Pointer)boolgcdata*byte//garbagecollectiondatastrnameOff//stringformptrToThistypeOff//typeforpointertothistype,maybezero}在64位程序和系统中占48个字节,其结构分布如下:在Go语言中,使用reflect.rtype结构来描述任何Go类型的基本信息。在Go语言中,使用reflect.structType结构来描述结构类(reflect.Struct)数据的类型信息,定义如下://structTyperepresentsastructtype.typestructTypestruct{rtypepkgPathnamefields[]structField//sortedbyoffset}//StructfieldtypestructFieldstruct{namename//nameisalwaysnon-emptytyp*rtype//typeoffieldoffsetEmbeduintptr//byteoffsetoffield<<1|isEmbedded}在64位程序和系统中占用80个字节,其结构分布如下:在之前的文章中,我已经详细介绍了type和method的相关内容。如果你还没有看过,建议你不要错过:讲整型,了解函数内存中的接口类型。在Go语言中,结构类型不仅可以包含字段,还可以定义方法。事实上,完整的类型信息结构分布如下:当然,该结构可能不包含字段或方法。环境操作系统:Ubuntu20.04.2LTS;x86_64Go:goversiongo1.16.2linux/amd64statement不同的操作系统、处理器架构、Go版本可能会导致相同源码编译运行时寄存器值、内存地址、数据结构等存在差异。本文仅包括64位系统架构下64位可执行程序的研究和分析。本文仅保证当前环境下学习过程中分析数据的准确性和有效性。代码列表是Go语言的,结构随处可见,所以本文的示例代码不再自定义结构,而是使用Go语言常用的结构进行演示。命令行参数详解 文章中已经详细介绍了flag.FlagSet结构。在本文中,我们将详细介绍flag.FlagSet和reflect.Value结构体的类型信息。packagemainimport("flag""fmt""reflect")funcmain(){f:=flag.FlagSet{}Print(reflect.TypeOf(f))Print(reflect.TypeOf(&f))_=f.Set("你好","world")f.PrintDefaults()fmt.Println(f.Args())v:=reflect.ValueOf(f)Print(reflect.TypeOf(v))Print(reflect.TypeOf(&v))Print(reflect.TypeOf(struct{}{}))}//go:noinlinefuncPrint(treflect.Type){fmt.Printf("Type=%s\t,address=%p\n",t,t)}从运行运行结果,我们可以看到:结构体flag.FlagSet的类型信息保存在Address0x4c2ac0中。结构指针*flag.FlagSet的类型信息存储在地址0x4c68e0处。结构reflect.Value的类型信息存储在地址0x4ca160处。结构指针*reflect.Value的类型信息存储在地址0x4c9c60处。匿名结构struct{}{}的类型信息存放在地址0x4b4140。内存分析在main函数入口处设置断点进行调试。让我们从一个简单的结构开始。匿名结构struct{}是既没有字段也没有方法,其类型信息数据如下:rtype.size=0x0(0)rtype.ptrdata=0x0(0)rtype.hash=0x27f6ac1brtype.tflag=tflagExtraStar|tflagRegularMemoryrtype.align=1rtype.fieldAlign=1rtype.kind=0x19(25)->reflect.Structrtype.equal=0x4d3100->runtime.memequal0rtype.gcdata=0x4ea04frtype.str=0x0000241f->“struct{}”rtype.ptrToThis=0x0(0x0)structType.pkgPath=0->""structType.fields=[]这是一个特殊的结构,没有字段,没有方法,不占用内存空间。明明是在主包中定义的,但是包路径信息是空的。结构分布如下:神奇的是,struct{}类型的对象是可以比较的,它的比较函数是runtime.memequal0,定义如下:funcmemequal0(p,qunsafe.Pointer)bool{returntrue}也就是说,所有struct{}类型的对象,无论它们在内存中的什么位置,无论它们何时被创建,总是相等的。仔细看还是挺有道理的。结构类型flag.FlagSet结构标志。FlagSet包含8个字段,其类型信息占用288个字节。rtype.size=0x60(96)rtype.ptrdata=0x60(96)rtype.hash=0x644236d1rtype.tflag=tflagUncommon|tflagExtraStar|tflagNamedrtype.align=8rtype.fieldAlign=8rtype.kind=0x19(25)->reflect.Structrtype.equal=nilrtype.gcdata=0x4e852crtype.str=0x32b0->“flag.FlagSet”rtype.ptrToThis=0x208e0(0x4c68e0)结构体ype.pkgPath=0x4a6368->"flag"structType.fields.Data=0x4c2b20structType.fields.Len=8->字节数structType.fields.Cap=8uncommonType.pkgpath=0x368->"flag"uncommonType.mcount=0->方法数量uncommonType.xcount=0uncommonType.moff=208structType.fields=[{name=0x4a69a0->Usagetyp=0x4b0140->func()offsetEmbed=0x0(0)},{name=0x4a69a0->nametyp=0x4b1220->stringoffsetEmbed=0x8(8)},{name=0x4a704a->parsedtyp=0x4b0460->booloffsetEmbed=0x18(24)},{name=0x4a6e64->actualtyp=0x4b4c20->map[string]*flag.FlagoffsetEmbed=0x20(32)},{name=0x4a6f0f->formaltyp=0x4b4c20->map[string]*flag.FlagoffsetEmbed=0x28(40)},{name=0x4a646d->argstyp=0x4afe00->[]stringoffsetEmbed=0x30(48)},{name=0x4a9450->errorHandlingtyp=0x4b05a0->flag.ErrorHandlingoffsetEmbed=0x48(72)},{name=0x4a702f->outputtyp=0x4b65c0->io.WriteroffsetEmbed=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字节的空间。从上面的Field数据可以看出,string类型的字段占16个字节,[]string类型的字段占24个字节,interface类型的字段占16个字节,这与分析中得到的结果一致上一篇文章。另外,可以看到map类型的字段居然占用了8个字节的空间。地图类型会在后面的文章中详细介绍。细心的读者可能已经注意到,flag.FlagSet类型没有任何方法,因为它的uncommonType.mcount=0。在flag/flag.go源文件中,不是定义了很多方法吗?上面的代码清单中,flag.FlagSet类型的对象f为什么会调用下面的方法?_=f.Set("hello","world")f.PrintDefaults()fmt.Println(f.Args())其实flag/flag.go源文件中定义的方法的receivers都是*flag.FlagSet指针类型,并没有flag.FlagSet类型。)[]string{returnf.args}flag.FlagSet类型的对象f可以调用*flag.FlagSet指针类型的方法,这只是编译器为了方便开发者而实现的,只是语法糖而已。在这个例子中,编译器会将flag.FlagSet类型的对象f的地址作为参数传递给*flag.FlagSet指针类型的方法。反之,编译器也支持。指针类型*flag.FlagSet为了方便查看类型信息,作者开发了一个gdb插件脚本。查看*flag.FlagSet类型的信息如下,一共包含38个方法,其中34个是public方法。这里就不一一介绍了。(gdb)infotype0x4c68e0interfaceType{rtype={size=0x8(8)ptrdata=0x8(8)hash=0xe05aa02ctflag=tflagUncommon|tflagRegularMemoryalign=8fieldAlign=8kind=ptrequal=0x403a00
