Golang empty struct 的底层原理和其使用
共 5351字,需浏览 11分钟
·
2024-06-29 19:30
在 Go 中,普通结构体通常占据一个内存块。但有一种特殊情况:如果是空结构体,其内存的占用大小就为零。为什么是这样呢?这样的空结构体有什么用?
type 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。
Example[1]
package 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 函数中:
//go:linkname mallocgc
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
这就是 Empty struct 的秘密。利用这个特殊变量,我们可以实现许多功能。
空结构和内存对齐
通常情况下,如果空结构体是较大结构体的一部分,则不会占用内存。但是,当空结构体是最后一个字段时,就会触发内存对齐。
Example[2]
type A struct {
x int
y string
z struct{}
}
type B struct {
x int
z struct{}
y string
}
func main() {
println(unsafe.Alignof(A{}))
println(unsafe.Alignof(B{}))
println(unsafe.Sizeof(A{}))
println(unsafe.Sizeof(B{}))
}
/**
8
8
32
24
**/
当存在指向字段的指针时,返回的地址可能在结构体之外,如果释放结构体时没有释放该内存,则可能导致内存泄漏。因此,当空结构体是另一个结构体的最后一个字段时,为了安全起见,会分配额外的内存。如果空结构体位于结构体的开头或中间,则其地址与下面的变量相同。
type A struct {
x int
y string
z struct{}
}
type B struct {
x int
z struct{}
y string
}
func main() {
a := A{}
b := B{}
fmt.Printf("%p\n", &a.y)
fmt.Printf("%p\n", &a.z)
fmt.Printf("%p\n", &b.y)
fmt.Printf("%p\n", &b.z)
}
/**
0x1400012c008
0x1400012c018
0x1400012e008
0x1400012e008
**/
空结构使用案例
空结构 struct struct{} 存在的核心原因是为了节省内存。当你需要一个结构但不关心其内容时,可以考虑使用空结构。Go 的核心复合结构,如 map、chan 和 slice,都可以使用 struct{}。
map & struct{}
// Create map
m := make(map[int]struct{})
// Assign value
m[1] = struct{}{}
// Check if key exists
_, ok := m[1]
chan & struct{}
典型的情况是将 channel 和 struct{} 结合在一起,其中 struct{} 经常被用作信号,而不关心其内容。正如前几篇文章所分析的,通道的基本数据结构是一个管理结构加一个环形缓冲区。如果 struct{} 被用作元素,则环形缓冲区为零分配。
chan 和 struct{} 放在一起的唯一用途是信号传输,因为空结构体本身不能携带任何值。一般情况下,它不用于缓冲通道。
// Create a signal channel
waitc := make(chan struct{})
// ...
goroutine 1:
// Send signal: push element
waitc <- struct{}{}
// Send signal: close
close(waitc)
goroutine 2:
select {
// Receive signal and perform corresponding actions
case <-waitc:
}
在这种情况下,有必要使用 struct{} 吗?其实不然,节省的内存几乎可以忽略不计。关键在于,我们并不关心 chan 的元素值,因此使用了 struct{}。
总结
-
空结构体仍然是大小为 0 的结构体。 -
所有空结构体共享同一个地址:zerobase 的地址。 -
我们可以利用 empty 结构不占用内存的特性来优化代码,例如使用映射来实现集合和通道。
example-1: https://go.dev/play/p/WNxfXviET_i
[2]example-2: https://go.dev/play/p/HcxlywljovS