Golang empty struct 的底層原理和其使用
共 5351字,需瀏覽 11分鐘
·
2024-06-29 19:30
在 Go 中,普通結(jié)構(gòu)體通常占據(jù)一個(gè)內(nèi)存塊。但有一種特殊情況:如果是空結(jié)構(gòu)體,其內(nèi)存的占用大小就為零。為什么是這樣呢?這樣的空結(jié)構(gòu)體有什么用?
type Test struct {
A int
B string
}
func main() {
fmt.Println(unsafe.Sizeof(Test{}))
fmt.Println(unsafe.Sizeof(struct{}{}))
}
/*
24
0
*/
空結(jié)構(gòu)的秘密
特殊變量:零基數(shù)
空結(jié)構(gòu)體是沒有內(nèi)存大小的結(jié)構(gòu)體。這種說法是正確的,但更準(zhǔn)確地說,它有一個(gè)特殊的起點(diǎn):zerobase 變量。這是一個(gè)占 8 字節(jié)的 uintptr 全局變量。每當(dāng)定義無數(shù)個(gè) struct {} 變量時(shí),編譯器都會(huì)分配這個(gè) zerobase 變量的地址。換句話說,在 Go 語言中,任何大小為 0 的內(nèi)存分配都使用相同的地址 &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
空結(jié)構(gòu)體變量的內(nèi)存地址都是相同的。這是因?yàn)榫幾g器在遇到這種特殊類型的內(nèi)存分配時(shí),會(huì)在編譯過程中分配 &zerobase。這一邏輯存在于 mallocgc 函數(shù)中:
//go:linkname mallocgc
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
這就是 Empty struct 的秘密。利用這個(gè)特殊變量,我們可以實(shí)現(xiàn)許多功能。
空結(jié)構(gòu)和內(nèi)存對(duì)齊
通常情況下,如果空結(jié)構(gòu)體是較大結(jié)構(gòu)體的一部分,則不會(huì)占用內(nèi)存。但是,當(dāng)空結(jié)構(gòu)體是最后一個(gè)字段時(shí),就會(huì)觸發(fā)內(nèi)存對(duì)齊。
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
**/
當(dāng)存在指向字段的指針時(shí),返回的地址可能在結(jié)構(gòu)體之外,如果釋放結(jié)構(gòu)體時(shí)沒有釋放該內(nèi)存,則可能導(dǎo)致內(nèi)存泄漏。因此,當(dāng)空結(jié)構(gòu)體是另一個(gè)結(jié)構(gòu)體的最后一個(gè)字段時(shí),為了安全起見,會(huì)分配額外的內(nèi)存。如果空結(jié)構(gòu)體位于結(jié)構(gòu)體的開頭或中間,則其地址與下面的變量相同。
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
**/
空結(jié)構(gòu)使用案例
空結(jié)構(gòu) struct struct{} 存在的核心原因是為了節(jié)省內(nèi)存。當(dāng)你需要一個(gè)結(jié)構(gòu)但不關(guān)心其內(nèi)容時(shí),可以考慮使用空結(jié)構(gòu)。Go 的核心復(fù)合結(jié)構(gòu),如 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{} 結(jié)合在一起,其中 struct{} 經(jīng)常被用作信號(hào),而不關(guān)心其內(nèi)容。正如前幾篇文章所分析的,通道的基本數(shù)據(jù)結(jié)構(gòu)是一個(gè)管理結(jié)構(gòu)加一個(gè)環(huán)形緩沖區(qū)。如果 struct{} 被用作元素,則環(huán)形緩沖區(qū)為零分配。
chan 和 struct{} 放在一起的唯一用途是信號(hào)傳輸,因?yàn)榭战Y(jié)構(gòu)體本身不能攜帶任何值。一般情況下,它不用于緩沖通道。
// 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{} 嗎?其實(shí)不然,節(jié)省的內(nèi)存幾乎可以忽略不計(jì)。關(guān)鍵在于,我們并不關(guān)心 chan 的元素值,因此使用了 struct{}。
總結(jié)
-
空結(jié)構(gòu)體仍然是大小為 0 的結(jié)構(gòu)體。 -
所有空結(jié)構(gòu)體共享同一個(gè)地址:zerobase 的地址。 -
我們可以利用 empty 結(jié)構(gòu)不占用內(nèi)存的特性來優(yōu)化代碼,例如使用映射來實(shí)現(xiàn)集合和通道。
example-1: https://go.dev/play/p/WNxfXviET_i
[2]example-2: https://go.dev/play/p/HcxlywljovS
