在 Go 中,普通结构体通常占据一个内存块。但有一种特殊情况:如果是空结构体,其大小为零。这怎么可能?空结构体有什么用?1
2
3
4
5
6
7
8
9
10
11
12
13
14type Test struct {
A int
B string
}
func main() {
fmt.Println(unsafe.Sizeof(Test{}))
fmt.Println(unsafe.Sizeof(struct{}{}))
}
/*
24
0
*/
空结构的秘密
特殊变量:零基数
空结构体是没有内存大小的结构体。这种说法是正确的,但更准确地说,它有一个特殊的起点:zerobase 变量。这是一个占 8 字节的 uintptr 全局变量。每当定义无数个 struct {} 变量时,编译器都会分配这个 zerobase 变量的地址。换句话说,在 Go 语言中,任何大小为 0 的内存分配都使用相同的地址 &zerobase。
Example1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import "fmt"
type emptyStruct struct {}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
}
// 0x58e360
// 0x58e360
// 0x58e360
空结构体变量的内存地址都是相同的。这是因为编译器在遇到这种特殊类型的内存分配时,会在编译过程中分配 &zerobase。这一逻辑存在于 mallocgc 函数中:
1 | //go:linkname mallocgc |
这就是 Empty struct 的秘密。利用这个特殊变量,我们可以实现许多功能。
空结构和内存对齐
通常情况下,如果空结构体是较大结构体的一部分,则不会占用内存。但是,当空结构体是最后一个字段时,就会触发内存对齐。
1 | type A struct { |
当存在指向字段的指针时,返回的地址可能在结构体之外,如果释放结构体时没有释放该内存,则可能导致内存泄漏。因此,当空结构体是另一个结构体的最后一个字段时,为了安全起见,会分配额外的内存。如果空结构体位于结构体的开头或中间,则其地址与下面的变量相同。
1 | type A struct { |
空结构使用案例
空结构 struct struct{} 存在的核心原因是为了节省内存。当你需要一个结构但不关心其内容时,可以考虑使用空结构。Go 的核心复合结构,如 map、chan 和 slice,都可以使用 struct{}。
map & struct{}
1 | // Create map |
chan & struct{}
典型的情况是将 channel 和 struct{} 结合在一起,其中 struct{} 经常被用作信号,而不关心其内容。正如前几篇文章所分析的,通道的基本数据结构是一个管理结构加一个环形缓冲区。如果 struct{} 被用作元素,则环形缓冲区为零分配。
chan 和 struct{} 放在一起的唯一用途是信号传输,因为空结构体本身不能携带任何值。一般情况下,它不用于缓冲通道。
1 | // Create a signal channel |
在这种情况下,有必要使用 struct{} 吗?其实不然,节省的内存几乎可以忽略不计。关键在于,我们并不关心 chan 的元素值,因此使用了 struct{}。
总结
- 空结构体仍然是大小为 0 的结构体。
- 所有空结构体共享同一个地址:zerobase 的地址。
- 我们可以利用 empty 结构不占用内存的特性来优化代码,例如使用映射来实现集合和通道。