理解Go中空結(jié)構(gòu)體的應用和實現(xiàn)原理
在實際項目或開源程序中,相信大家都見過將一個空結(jié)構(gòu)體作為map值的場景:
// CanSkipFuncs will skip valid if RequiredFirst is true and the struct field's value is empty
var CanSkipFuncs = map[string]struct{}{
"Email": {},
"IP": {},
"Mobile": {},
"Tel": {},
"Phone": {},
"ZipCode": {},
}
或?qū)⒁粋€空結(jié)構(gòu)體寫入到通道中的使用:
w.ch <- struct{}{}
那為什么要這樣使用空結(jié)構(gòu)體呢?今天就跟大家一起來學習下空結(jié)構(gòu)體的應用以及底層原理。
1 什么空結(jié)構(gòu)體
首先來看看空結(jié)構(gòu)體是什么。空結(jié)構(gòu)體也是結(jié)構(gòu)體類型,具有結(jié)構(gòu)體的一切特性。但該結(jié)構(gòu)體中沒有任何字段組合。所以,該空結(jié)構(gòu)體類型的變量占用的空間為0。
我們通過unsafe.Sizeof函數(shù)來驗證一下。unsafe.Sizeof函數(shù)的作用是返回一個數(shù)據(jù)類型所占的空間大小。我們驗證一下:
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // prints 0
我們看到打印的結(jié)果是0,表明struct{}的類型占用的空間是0。
我們還可以通過reflect的類型來驗證。
var s struct{}
typ := reflect.TypeOf(s)
fmt.Println(typ.Size()) // 0
我們看到,通過映射變量s的類型,輸出空類型的空間大小也是0。
2 空結(jié)構(gòu)體類型變量的地址
我們知道,在編程語言中,變量的作用就是在內(nèi)存中,標記和存儲數(shù)據(jù)的。也就是說每個變量會對應著一塊內(nèi)存空間,既然是內(nèi)存空間,那就應該有對應的內(nèi)存地址。那空結(jié)構(gòu)體類型變量的地址是什么呢?我們通過如下代碼來看下:
package main
import (
"fmt"
"unsafe"
)
type emptyStruct struct{}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Println(a)
fmt.Printf("%pn", &a) //0x116be80
fmt.Printf("%pn", &b) //0x116be80
fmt.Printf("%pn", &c) //0x116be80
fmt.Println(a == b) //true
}
我們發(fā)現(xiàn),所有空結(jié)構(gòu)體類型的變量地址都是一樣的。那這是為什么呢?
在底層實現(xiàn)中,這和一個很重要的 zerobase 變量有關(guān)(在runtime里多次使用到了這個變量),而zerobase 變量是一個 uintptr 的全局變量,占用8個字節(jié)。在go源碼src/runtime/malloc.go中有如下定義:
// base address for all 0-byte allocations
var zerobase uintptr
只要你將struct{} 賦值給一個或者多個變量,它都返回這個 zerobase 的地址,這點我們上面已經(jīng)證實過這一點了。
在golang中大量的地方使用到了這個 zerobase 變量,只要分配的內(nèi)存為0,就返回這個變量地址,在go源碼src/runtime/malloc.go的mallocgc函數(shù)中定義如下:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
3 空結(jié)構(gòu)體的應用場景
一般我們用在用戶不關(guān)注值內(nèi)容的情況下,只是作為一個信號或一個占位符來使用。
- 基于map實現(xiàn)集合功能。
- 與channel組合使用,實現(xiàn)一個信號
基于map實現(xiàn)集合功能就是我們開頭提到的。使用空結(jié)構(gòu)體不占用存儲空間外,還有一個語義上的原因。例如:
var CanSkipFuncs = map[string]bool{
"Email": true,
"IP": true,
"Mobile": true,
"Tel": false,
"Phone": false,
"ZipCode": false,
}
我們這里將空結(jié)構(gòu)體類型更換成布爾類型。首先,聲明下,CanSkipFuncs集合代表的是所有要跳過的函數(shù)。所以這里的值設置成true還是false是沒有任何影響的。
那么當閱讀或review代碼的時候,很有可能帶來疑惑,對于值所表達的意圖就有所懷疑,增加了理解代碼的難度。就會理解成當值為true時會執(zhí)行一個分支,當值為false時會執(zhí)行另一段邏輯。而相比使用一個空結(jié)構(gòu)體strcut{}理解起來會更容易,一看空結(jié)構(gòu)體struct{}就知道要表達的意思是不需要關(guān)心值是什么,只需要關(guān)心鍵值即可。
我們再來看下和channel組合使用的例子。在etcd項目中,就有通過往channel中寫入一個空結(jié)構(gòu)體作為信號的,源碼位于/etcd/server/auth/simple_token.go中,如下:
func (tm *simpleTokenTTLKeeper) stop() {
select {
case tm.stopc <- struct{}{}:
case <-tm.donec:
}
<-tm.donec
}
還有一種是基于緩沖channel實現(xiàn)并發(fā)限速。如下:
var limit = make(chan struct{}, 3)
func main() {
// …………
for _, w := range work {
go func() {
limit <- struct{}{}
w()
<-limit
}()
}
// …………
}
4 總結(jié)
空結(jié)構(gòu)體是一種不包含任何字段的結(jié)構(gòu)體類型,不僅具有結(jié)構(gòu)體類型的一切屬性,而且該結(jié)構(gòu)體類型占用的空間為0。常被用于map的集合或和通道配合使用發(fā)送信號使用的場景。
參考鏈接:
https://blog.haohtml.com/archives/20339
https://ijayer.github.io/post/tech/code/golang/20200419_emtpy_struct_in_go/