结构体
所谓结构体,实际上就是由各种类型的数据组合而成的一种复合数据类型.
在数据存储上来讲,结构体和数组没有太大的区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,所以只能通过字段的相对偏移量进行访问. 而数组的各个元素类型相同,可以通过索引快速访问,实际其本质上也是通过相对偏移量计算地址进行访问.
因为结构体的各个字段类型不同,有大有小,而结构体在存储时通常需要进行内存对齐,所以结构体在存储时可能会出现"空洞",也就是无法使用到的内存空间.
在之前的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
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
在64位程序和系统中占48个字节,其结构分布如下:
在Go语言中,使用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
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
在64位程序和系统中占80个字节,其结构分布如下:
在之前的几篇文章中,已经详细介绍了类型方法相关内容,如果还未阅读,建议不要错过:
- 再谈整数类型
- 深入理解函数
- 内存中的接口类型
在Go语言中,结构体类型不但可以包含字段,还可以定义方法,实际上完整的类型信息结构分布如下:
当然,结构体是可以不包含字段的,也可以没有方法的.
环境
OS : Ubuntu 20.04.2 LTS; x86_64
Go : go version go1.16.2 linux/amd64
- 1.
- 2.
声明
操作系统、处理器架构、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)
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
运行
从运行结果可以看到:
- 结构体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
}
- 1.
- 2.
- 3.
也就是说,所有的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)
}
]
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
从以上数据可以看到,结构体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())
- 1.
- 2.
- 3.
实际上,flag/flag.go源文件中定义的方法的receiver都是*flag.FlagSet指针类型,没有flag.FlagSet类型.
// Args returns the non-flag arguments.
func (f *FlagSet) Args() []string { return f.args }
- 1.
- 2.
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
}
]
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
结构体类型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>
},
......
]
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
再看*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>
},
......
]
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
我们可以清楚地看到,在源码中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()})
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
通过reflect.Value类型的对象调用时,实际可能执行的两个方法中的任何一个.
通过*reflect.Value类型的指针对象调用时,也可能执行的两个方法中的任何一个.
这完全是由编译器决定的.
但是通过接口调用时,执行的一定是reflect.(*Value).Len这个方法的指令集合.
自定义结构体千变万化,但是结构体类型信息相对还是单一,容易理解.