在 Go 中,普通结构体通常占据一个内存块。但有一种特殊情况:如果是空结构体,其大小为零。这怎么可能?空结构体有什么用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 函数中:
1 2 3 4 5 6 7 //go:linkname mallocgc func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size == 0 { return unsafe.Pointer(&zerobase) } ...
这就是 Empty struct 的秘密。利用这个特殊变量,我们可以实现许多功能。
空结构和内存对齐通常情况下,如果空结构体是较大结构体的一部分,则不会占用内存。但是,当空结构体是最后一个字段时,就会触发内存对齐。
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 **/
当存在指向字段的指针时,返回的地址可能在结构体之外,如果释放结构体时没有释放该内存,则可能导致内存泄漏。因此,当空结构体是另一个结构体的最后一个字段时,为了安全起见,会分配额外的内存。如果空结构体位于结构体的开头或中间,则其地址与下面的变量相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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{}1 2 3 4 5 6 // 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{} 放在一起的唯一用途是信号传输,因为空结构体本身不能携带任何值。一般情况下,它不用于缓冲通道。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 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 结构不占用内存的特性来优化代码,例如使用映射来实现集合和通道。
-------------The End-------------
subscribe to my blog by scanning my public wechat account
0 %
文章来源: https://cloudsjhan.github.io/2024/06/20/Golang-%E7%A9%BA%E7%BB%93%E6%9E%84%E4%BD%93%E7%9A%84%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E5%92%8C%E5%85%B6%E4%BD%BF%E7%94%A8/ 如有侵权请联系:admin#unsafe.sh