空結構體是沒有欄位的結構體,以下是空結構體的兩個例子:
type Q struct{}
var q struct{}
但是如果一個結構體沒有欄位,不包含任何數據,那麼它的用處是什麼?我們能夠利用空結構體完成什麼任務?
一、背景在深入研究空結構體之前,先簡短的介紹一下關於結構體寬度的知識。 術語寬度來自於gc編譯器,但是他的詞源可以追溯到幾十年以前。寬度描述了存儲一個數據類型實例需要佔用的比特數,由於進程的內存空間是一維的,這裡更傾向於將寬度理解為Size。
寬度是數據類型的一個屬性。Go程序中所有的實例都是一種數據類型,一個實例的寬度是由他的數據類型決定的,通常是8bit的整數倍。
我們可以通過unsafe.Sizeof()函數獲取任何實例的寬度:
var s string
var c complex128
fmt.Println(unsafe.Sizeof(s)) // prints 8
fmt.Println(unsafe.Sizeof(c)) // prints 16
結構體提供了定義組合類型的靈活方式,組合類型的寬度是欄位寬度的和,然後再加上填充寬度:
type S struct {
a uint16
b uint32
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 8, not 6
現在我們可以知道由於空結構體中沒有任何欄位,所以空結構體的寬度是0,它佔用了0位元組的內存空間。var s struct{} fmt.Println(unsafe.Sizeof(s)) // prints 0 由於空結構體佔用0位元組,那麼空結構體作為欄位也不需要佔用內存空間。空結構體組成的組合數據類型也不會佔用內存空間。
type S struct {
A struct{}
B struct{}
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 0
由於Go的正交性,空結構體可以像其他結構體一樣正常使用。正常結構體擁有的屬性,空結構體一樣具有。你可以定義一個空結構體組成的數組,當然這個切片不佔用內存空間。
var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // prints 0
空結構體組成的切片的寬度只是他的頭部數據的長度,就像上例展示的那樣,切片元素不佔用內存空間。
var x = make([]struct{}, 1000000000)
fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground
當然切片的內置子切片、長度和容量等屬性依舊可以工作。
ar x = make([]struct{}, 100)
var y = x[:50]
fmt.Println(len(y), cap(y)) // prints 50 100
你甚至可以尋址一個空結構體,就像其他類型的實例一樣。
var a struct{}
var b = &a
空結構體作為切片的元素也具有一樣的屬性。
a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println(&a == &b) // false, a and b are different slices
fmt.Println(&a[0] == &b[0]) // true, their backing arrays are the same
因為空結構體不包含欄位,所以不存儲數據。如果空結構體不包含數據,那麼就沒有辦法說兩個空結構體的值不相等,所以任意兩個空結構體的值相等。
a := struct{}{} // not the zero value, a real new struct{} instance
b := struct{}{}
fmt.Println(a == b) // true
空結構體也可以作為方法的接收者。
type S struct{}
func (s *S) addr() { fmt.Printf("%p\n", s) }
func main() {
var a, b S
a.addr()
b.addr()
}
在Go語言中,有一種特殊的struct{}類型的channel。定義:var sig = make(chan struct{}) 使用空 struct 是對內存更友好的開發方式,在 go 原始碼中針對 空struct 類數據內存申請部分,返回地址都是一個固定的地址。那麼就避免了可能的內存浪費。
舉例:
package main
import "fmt"
import "time"
var strChan = make(chan string,3)
func main(){
syncChan1 := make(chan struct{},1) //接收同步變量
syncChan2 := make(chan struct{},2) //主線程啟動了兩個goruntime線程,
//等這兩個goruntime線程結束後主線程才能結束
//用於演示接受操作
go func(){
<- syncChan1 //表示可以開始接收數據了,否則等待
fmt.Println("[receiver] Received a sync signal and wait a second...")
time.Sleep(time.Second)
for{
if elem,ok := <-strChan;ok{
fmt.Println("[receiver] Received:",elem)
}else{
break
}
}
fmt.Println("[receiver] Stopped.")
syncChan2 <- struct{}{}
}()
//用於演示發送操作
go func(){
for i,elem := range []string{"a","b","c","d"}{
fmt.Println("[sender] Sent:",elem)
strChan <- elem
if (i+1)%3==0 {
syncChan1 <- struct{}{}
fmt.Println("[sender] Sent a sync signal. wait 1 secnd...")
time.Sleep(time.Second)
}
}
fmt.Println("[sender] wait 2 seconds...")
time.Sleep(time.Second)
close(strChan)
syncChan2 <- struct{}{}
}()
//主線程等待發送線程和接收線程結束後再結束
fmt.Println("[main] waiting...")
<- syncChan2
<- syncChan2
fmt.Println("[main] stoped")
}
運行結果:
[main] waiting...
[sender] Sent: a
[sender] Sent: b
[sender] Sent: c
[sender] Sent a sync signal. wait 1 secnd...
[receiver] Received a sync signal and wait a second...
[receiver] Received: a
[receiver] Received: b
[receiver] Received: c
[sender] Sent: d
[sender] wait 2 seconds...
[receiver] Received: d
[receiver] Stopped.
[main] stoped
struch{}代表不包含任何欄位的結構體類型,也可稱為空結構體類型。在go語言中,空結構體類型是不佔用系統內存的,並且所有該類型的變量都擁有相同的內存地址。建議用於傳遞信號的通道都以struct{}作為元素類型,除非需要傳遞更多的信息
發送方向通道發送的值會被複製,接收方接收到的總是該值的副本,而不是該值本身。經由通道傳遞的值最少會被複製一次,最多會被複製兩次。例如,當向一個已空的通道發送值,且已有至少一個接收方因此等待時,該通道會繞過本身的緩衝隊列,直接把這個值複製給最早等待的那個接收方,這種情況傳遞的值只複製一次;當從一個已滿的通道接收值,且已有至少一個發送方因此等待時,該通道會把緩衝隊列中最早進入的那個值複製給接收方,再把最早等待的發送方要發送的數據複製到那個值得原先位置上(通道的緩衝隊列屬於環形隊列,這樣做是沒有問題的),這種情況傳遞的值複製兩次。
通道傳遞是複製傳遞的值。因此如果傳遞的是值類型,接收方對該值修改不會影響發送方持有的值;如果傳遞的是引用類型,則發送方或者接收方對該對象的修改會影響雙方所持有的對象
我們是程式設計師khaos,一群酷愛編程,樂於分享的小夥子,下期見~
「分享」「點讚」「在看」是最大支持