这是由于棘手的填充造成的。
首先请允许我稍微重命名结构和字段,以便于讨论它们:
type bar1 struct {
A [0]byte
I int
}
type bar2 struct {
I int
A [0]byte
}
这当然不会改变大小和偏移量,可以在 Go Playground 上验证:
bar1 size: 4
bar1.A offset: 0
bar1.I offset: 0
bar2 size: 8
bar2.I offset: 0
bar2.A offset: 4
[0]byte 类型的值的大小为零,因此在bar1 中不为第一个字段(bar1.A)保留任何空间并将bar1.I 字段布局为 0 是完全有效的偏移量。
问题是:为什么编译器不能在第二种情况下做同样的事情(bar2)?
字段必须有一个地址,该地址必须位于为前一个字段保留的内存区域之后。在第一种情况下,第一个字段 bar1.A 的大小为 0,因此第二个字段的偏移量可能为 0,它不会与第一个字段“重叠”。
在bar2 的情况下,第二个字段不能有一个与第一个字段重叠的地址(因此是偏移量),因此它的偏移量不能小于int 的大小,如果是 4 个字节32 位架构(在 64 位架构的情况下为 8 个字节)。
这似乎还可以。但是既然bar2.A 的大小为零,为什么结构bar2 的大小不能是这样:4 个字节(或64 位拱形中的8 个)?
这是因为获取大小为 0 的字段(和变量)的地址是完全有效的。好的,那又怎样?
如果是bar2,编译器必须插入一个4(或8)字节的填充,否则使用bar2.A字段的地址将指向内存区域之外保留给bar2 类型的值。
例如,无填充bar2 的值可能具有0x100 的地址,大小为4,因此为结构值保留的内存具有地址范围0x100 .. 0x103。 bar2.A 的地址是 0x104,它在结构的内存之外。对于此结构的数组(例如x [5]bar2),如果数组从0x100开始,则x[0]的地址为0x100,x[0].A的地址为0x104,而后续元素 x[1] 也将是 0x104 但这是另一个结构值的地址!不酷。
为了避免这种情况,编译器会插入一个填充(取决于拱门,它会是 4 或 8 个字节),以便获取 bar2.A 的地址不会导致地址超出结构的内存,否则可能会引发问题并导致有关垃圾收集的问题(例如,如果只保留 bar2.A 的地址,但不保留结构或指向它或其其他字段的另一个指针,则不应对整个结构进行垃圾收集,但因为没有指针指向它内存区域,这样做似乎是有效的)。插入的填充将是 4(或 8)字节,因为Spec: Size and alignment guarantees:
对于结构类型的变量x:unsafe.Alignof(x) 是x 中每个字段f 的所有值unsafe.Alignof(x.f) 中的最大值,但至少是1。
如果是这样,添加一个额外的int 字段将使两个结构的大小相等:
type bar1 struct {
I int
A [0]byte
X int
}
type bar2 struct {
A [0]byte
I int
X int
}
确实,它们在 32 位拱门上都有 8 个字节(在 64 位拱门上有 16 个字节)(在 Go Playground 上试试):
bar1 size: 8
bar1.I offset: 0
bar1.A offset: 4
bar1.X offset: 4
bar2 size: 8
bar2.A offset: 0
bar2.I offset: 0
bar2.X offset: 4
查看相关问题:Struct has different size if the field order is different