前言:
在 Go 語言中,有一個比較特殊的類型,經常會有剛接觸 Go 的小伙伴問到,又或是不理解。
他就是 Go 里的空結構體(struct
)的使用,常常會有看到有人使用:
1
|
ch := make (chan struct{}) |
還清一色的使用結構體,也不用其他類型。高度常見,也就不是一個偶發現象了,肯定是背后必然有什么原因。
1、為什么使用
說白了,就是希望節省空間。但,新問題又來了,為什么不能用其他的類型來做?
這就涉及到在 Go
語言中 ”寬度“ 的概念,寬度描述了一個類型的實例所占用的存儲空間的字節數。
寬度是一個類型的屬性。在 Go 語言中的每個值都有一個類型,值的寬度由其類型定義,并且總是 8 bits
的倍數。
在 Go 語言中我們可以借助 unsafe.Sizeof
方法,來獲取:
1
2
3
4
5
6
7
|
// Sizeof takes an expression x of any type and returns the size in bytes // of a hypothetical variable v as if v was declared via var v = x. // The size does not include any memory possibly referenced by x. // For instance, if x is a slice, Sizeof returns the size of the slice // descriptor, not the size of the memory referenced by the slice. // The return value of Sizeof is a Go constant. func Sizeof(x ArbitraryType) uintptr |
該方法能夠得到值的寬度,自然而然也就能知道其類型對應的寬度是多少了。
我們對應看看 Go 語言中幾種常見的類型寬度大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() { var a int var b string var c bool var d [3]int32 var e []string var f map[string]bool fmt .Println( unsafe.Sizeof(a), unsafe.Sizeof(b), unsafe.Sizeof(c), unsafe.Sizeof(d), unsafe.Sizeof(e), unsafe.Sizeof(f), ) } |
輸出結果:
8 16 1 12 24 8
你可以發現我們列舉的幾種類型,只是單純聲明,我們也啥沒干,依然占據一定的寬度。
如果我們的場景,只是占位符,那怎么辦,系統里的開銷就這么白白浪費了?
2、空結構體的特殊性
空結構體在各類系統中頻繁出現的原因之一,就是需要一個占位符。而恰恰好,Go 空結構體的寬度是特殊的。
如下:
1
2
3
4
|
func main() { var s struct{} fmt .Println(unsafe.Sizeof(s)) } |
輸出結果:
0
空結構體的寬度是很直接了當的 0,即便是變形處理:
1
2
3
4
5
6
7
8
9
|
type S struct { A struct{} B struct{} } func main() { var s S fmt .Println(unsafe.Sizeof(s)) } |
其最終輸出結果也是 0,完美切合人們對占位符的基本訴求,就是占著坑位,滿足基本輸入輸出就好。
但這時候問題又出現了,為什么只有空結構會有這種特殊待遇,其他類型又不行?
這是 Go 編譯器在內存分配時做的優化項
1
2
3
4
5
6
7
8
9
|
// base address for all 0-byte allocations var zerobase uintptr func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size == 0 { return unsafe.Pointer(&zerobase) } } |
當發現 size
為 0 時,會直接返回變量 zerobase
的引用,該變量是所有 0 字節的基準地址,不占據任何寬度。
因此空結構體的廣泛使用,是 Go 開發者們借助了這個小優化,達到了占位符的目的。
3、使用場景
了解清楚為什么空結構作為占位符使用的原因后,我們更進一步了解其真實的使用場景有哪些。
主要分為三塊:
- 實現方法接收者。
- 實現集合類型。
- 實現空通道。
3.1 實現方法接收者
在業務場景下,我們需要將方法組合起來,代表其是一個 ”分組“ 的,便于后續拓展和維護。
但是如果我們使用:
1
2
3
|
type T string func (s *T) Call() |
又似乎有點不大友好,因為作為一個字符串類型,其本身會占據定的空間。
這種時候我們會采用空結構體的方式,這樣也便于未來針對該類型進行公共字段等的增加。如下:
1
2
3
4
5
6
7
8
9
10
|
type T struct{} func (s *T) Call() { fmt .Println( "腦子進煎魚了" ) } func main() { var s T s.Call() } |
在該場景下,使用空結構體從多維度來考量是最合適的,易拓展,省空間,最結構化。
另外你會發現,其實你在日常開發中下意識就已經這么做了,你可以理解為設計模式和日常生活相結合的另類案例。
3.2 實現集合類型
在 Go 語言的標準庫中并沒有提供集合(Set
)的相關實現,因此一般在代碼中我們圖方便,會直接用 map
來替代。
但有個問題,就是集合類型的使用,只需要用到 key
(鍵),不需要 value
(值)。
這就是空結構體大戰身手的場景了:
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 Set map[string]struct{} func (s Set) Append(k string) { s[k] = struct{}{} } func (s Set) Remove(k string) { delete(s, k) } func (s Set) Exist(k string) bool { _, ok := s[k] return ok } func main() { set := Set{} set .Append( "煎魚" ) set .Append( "咸魚" ) set .Append( "蒸魚" ) set .Remove( "煎魚" ) fmt .Println( set .Exist( "煎魚" )) } |
空結構體作為占位符,不會額外增加不必要的內存開銷,很方便的就是解決了。
3.3 實現空通道
在 Go channel
的使用場景中,常常會遇到通知型 channel
,其不需要發送任何數據,只是用于協調 Goroutine
的運行,用于流轉各類狀態或是控制并發情況。
如下:
1
2
3
4
5
6
7
8
9
10
11
|
func main() { ch := make (chan struct{}) go func() { time .Sleep(1 * time .Second) close(ch) }() fmt .Println( "腦子好像進..." ) <-ch fmt .Println( "煎魚了!" ) } |
輸出結果:
腦子好像進...
煎魚了!
該程序會先輸出 ”腦子好像進...“ 后,再睡眠一段時間再輸出 "煎魚了!",達到間斷控制 的效果。 channel
由于該 channel
使用的是空結構體,因此也不會帶來額外的內存開銷。
到此這篇關于關于Go 空結構體的 3 種使用場景的文章就介紹到這了,更多相關Go 空結構體的 3 種使用場景內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/7018037514246029349