Golang中非零大小的空结构体解析

Golang中非零大小的空结构体解析 我正在尝试理解 Go 语言结构体的内存布局。据我所知,Go 在内存布局方面与 C 语言有一些相似之处,每种类型都有大小和对齐要求,并且会在结构体中添加一些填充(padding),以确保每个字段都位于正确的对齐位置。但是,我无法解释下面这种情况:

// Available at: https://play.golang.com/p/aiwYrIi6DX0
package main

import (
	"fmt"
	"unsafe"
)

type A struct {
	a int32
	b int32
	c struct{}
}

type B struct {
	c struct{}
	a int32
	b int32
}

func main() {
	fmt.Println(unsafe.Sizeof(A{})) // prints 12
	fmt.Println(unsafe.Sizeof(B{})) // prints 8
}

据我所知,struct{} 的大小为 0,并且按 1 字节对齐,但为什么结构体 A 的大小是 12,而结构体 B 是 8?看起来,如果一个结构体以一个空结构体结尾,他们会在结构体的末尾添加一些填充。但我不确定 Go 为什么要这样做。我的假设正确吗?如果正确,原因是什么?


更多关于Golang中非零大小的空结构体解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

请看:

stackoverflow.com

同样来自那个回答:

https://dave.cheney.net/2015/10/09/padding-is-hard

以及为什么编译器不会自动处理这个问题:

github.com/golang/go

更多关于Golang中非零大小的空结构体解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据以上文章,问题似乎源于垃圾回收机制。如果我们不用 [1]byte 填充最后一个 struct{},就有可能获取到指向无效内存区域的最后一个结构体的地址。但是,我仍然不明白为什么指向无效内存区域的指针是不行的。既然大小为零,解引用它就像空操作一样,我们可以直接返回 struct{}{} 而无需实际从内存中获取。而且,由于垃圾回收器拥有类型信息,它们可以推断出那些特定的内存区域是 struct{} 类型,在进行垃圾回收时不应该扫描。

我认为可能有问题的一种情况是当我们有 []A 时,&slice_of_a[0].c 可能指向 &slice_of_a[1],最终我们得到一个地址对应两种类型,一种是 struct{},一种是 A。但是,即使发生这种情况,我也不明白为什么这对垃圾回收机制有害。

我是不是遗漏了什么?

在 Go 语言中,空结构体 struct{} 的大小确实为 0,但其对齐方式和对结构体整体大小的影响取决于它在结构体中的位置。你的观察是正确的:当空结构体作为结构体的最后一个字段时,编译器会添加额外的填充,这主要是为了确保结构体在数组或切片中时,每个元素的地址都是唯一的。

原因分析

当空结构体作为结构体的最后一个字段时,Go 编译器会添加一个字节的填充,以确保如果该结构体被放入数组或切片中,每个元素的地址不会重叠。这是因为空结构体本身不占用空间,但为了满足指针的唯一性,需要保证每个数组元素的地址不同。

示例解析

在你的代码中:

  • 结构体 A:空结构体 c 是最后一个字段,编译器在 c 后添加填充,使结构体大小变为 12 字节(int32 各占 4 字节,对齐到 4 字节边界,空结构体后填充 4 字节)。
  • 结构体 B:空结构体 c 是第一个字段,不触发末尾填充规则,因此结构体大小为 8 字节(两个 int32 各占 4 字节,对齐到 4 字节边界)。

验证示例

以下代码进一步验证了空结构体在不同位置的影响:

package main

import (
	"fmt"
	"unsafe"
)

type A struct {
	a int32
	b int32
	c struct{}
}

type B struct {
	c struct{}
	a int32
	b int32
}

type C struct {
	a int32
	c struct{}
	b int32
}

func main() {
	fmt.Println("Size of A:", unsafe.Sizeof(A{})) // 12
	fmt.Println("Size of B:", unsafe.Sizeof(B{})) // 8
	fmt.Println("Size of C:", unsafe.Sizeof(C{})) // 8
}

输出:

Size of A: 12
Size of B: 8
Size of C: 8

在结构体 C 中,空结构体位于中间,不触发末尾填充,因此大小仍为 8 字节。

数组中的地址唯一性示例

以下代码展示了空结构体在数组中的地址行为:

package main

import (
	"fmt"
	"unsafe"
)

type Item struct {
	value int32
	empty struct{}
}

func main() {
	arr := [2]Item{}
	fmt.Printf("arr[0] address: %p\n", &arr[0])
	fmt.Printf("arr[1] address: %p\n", &arr[1])
	fmt.Println("Size of Item:", unsafe.Sizeof(Item{})) // 8
}

输出(地址示例):

arr[0] address: 0xc0000180a0
arr[1] address: 0xc0000180a8

每个数组元素的地址相差 8 字节,即使 empty 字段大小为 0,也保证了地址的唯一性。

总结

空结构体在结构体末尾时会触发填充,以确保在数组或切片中每个元素的地址唯一。这是 Go 语言内存安全性和指针语义的一部分。如果你的结构体不需要放入数组或切片,且希望节省内存,可以避免将空结构体放在末尾。

回到顶部