我们都知道,在Go语言中,空的结构体不占用任何存储空间,比如:
func main() {
a := A{}
fmt.Printf("Size of a: %d", unsafe.Sizeof(a))
}
type A struct{}
运行结果:
Size of a: 0
但是,在某些情况下,以上结论并不是完全正确,让我们来看一个例子:
type MyStruct struct {
Flag uint32
PropA uint64
PropB uint64
PropC uint64
PropD struct{}
}
结构体MyStruct
到底占用多少空间呢,在我64位的机器运行结果如下:
Size of mystruct: 40
类型MyStruct
中包含的都是固定大小的字段,Flag
是uint32
类型,占用4字节大小,PropA
, PropB
, PropC
都是uint64
类型,分别占用8字节大小,PropD
是空结构体,不占用存储空间。因此理论上,该类型占用都总存储空间大小应该是: 8+8+8+4 = 28 字节,那为何程序编译后发现实际上占用了40个字节大小呢?
内存对齐
产生这种差异的原因在于内存对齐。Go语言规范要求,结构体字段的地址必须是内存对齐的。因此,在64位机器上,任何uint64
字段的地址必须是8的倍数。实际上编译器看到的结构体像这个样子:
type MyStruct struct {
Flag uint32
_ [4]byte // 由编译器添加用于内存对齐
PropA uint64
PropB uint64
PropC uint64
PropD struct{}
}
此时理论上结构体占用的内存大小应为:Flag(4 bytes) + PropA(8 bytes) + PropB(64 bytes) + PropC(64 bytes) + 4 bytes 用于对齐的空间 = 32 bytes。但是我们知道之前程序运行的结果是40 bytes,那么多的4 bytes空间去哪了?
为了说明这个问题,让我们首先重新排列一下MyStruct
结构体的字段顺序:
type MyStruct struct {
Flag uint32
PropD struct{}
PropA uint64
PropB uint64
PropC uint64
}
此时检查结构体大小,发现结果为:
Size of mystruct: 32
嗯,看起来有进步。我们已经设法减少了MyStruct
结构体的存储空间,为什么会这样?
空结构体
你可能已经猜到,额外的存储来自于字段PropD
,它是一个类型为struct{}
的字段。在结构体底部存在一个空结构体字段会导致它的存储空间增加,什么意思?
答案是,虽然空结构体字段不占用存储空间,但你可以合法地取得它的地址。也就是说,假设有如下类型:
type S struct {
Foo struct{}
Bar struct{}
}
var s S
取s.Foo的地址是完全有效的,因此,s.Foo的地址将超出结构体的末尾!虽然Go代码无法对此无效地址执行任何操作,但是它毕竟是一个指针,垃圾收集器必须回收它,这可能导致内存泄漏或垃圾收集器崩溃。
关于这个问题的详细讨论在issue 9401中,并在Go 1.5中进行了修复。简单来说,对于任何占用0大小空间的类型,像struct {}
或者[0]byte
这些,如果该类型出现在结构体末尾,那么我们就假设它占用1个字节的大小。
因此,对于之前的结构体MyStruct
,它实际上看起来是这个样子:
type MyStruct struct {
Flag uint32
PropA uint64
PropB uint64
PropC uint64
// PropD struct{}
PropD [1]byte
}
加上内存对齐,对编译器来说就是:
type MyStruct struct {
Flag uint32
_ [4]byte // 编译器添加用于内存对齐
PropA uint64
PropB uint64
PropC uint64
// PropD struct{}
PropD [1]byte
_ [7]byte // 编译器添加,结构体整体也需要内存对齐
}
一共占用40个字节。
结论:不要在结构体定义的最后添加零大小的类型
网友评论