GO的空結構體struct{}到底有什麼用?

2022-01-18 程式設計師khaos
前言

空結構體是沒有欄位的結構體,以下是空結構體的兩個例子:

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()
}

四、chan struct{}

在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,一群酷愛編程,樂於分享的小夥子,下期見~


「分享」「點讚」「在看」是最大支持

相關焦點

  • Go 語言學習之 struct
    ,struct 中的每個變量稱為 struct 的成員變量。每個成員變量的欄位名都是固定且唯一的,每個成員變量都會用一個內置類型或自定義類型來聲明,並且支持使用自身的指針類型作為成員變量的類型,成員變量的欄位名和排列順序屬於 struct 類型組成部分。
  • 結構體成員賦值到底是深拷貝還是淺拷貝?
    來源:公眾號【編程珠璣】作者:守望先生ID:shouwangxiansheng在《C語言容易忽略的知識點》一文中,有讀者說這種結構體複雜成員賦值的的拷貝是淺拷貝(感謝讀者提出),那麼到底什麼是深拷貝,什麼是淺拷貝?
  • GO編程模式系列(一):切片,接口,時間和性能
    type slice struct{   array unsafe.Pointer //指向存放數據的數組指針   len   int           //長度有多大   cap   int           //容量有多大}用圖示來看,一個空的slice的表現如下:
  • 「C語言指針」起別名關鍵字typedef和結構體類型的恩怨情仇
    對於用戶定義的結構體struct student;我們覺得書寫不便,很麻煩,所以也可以利用typedef為它起別名:typedef struct student ST;這樣子,我們在後續使用學生這種類型的數據類型的時候,就不用再寫那麼一長串,轉而去使用我們起好的名字ST。
  • Go語言入門分享
    /hello就可以執行了;或者直接 go run hello.go 合二為一去執行。執行這個命令並不需要設置環境變量就可以了。看起來和c差不多,但是和Java不一樣,運行的時候不需要虛擬機。早期的GO工程也是使用Makefile來編譯,後來有了強大的命令 go build、go run,可以直接識別目錄還是文件。
  • 第六篇:C語言中結構體與文件操作相關知識點梳理
    注意:這個學生信息是一個整體,用前面學到的基本數據類型是無法實現的。有沒有一種學生的數據類型可以使用?系統沒有定義,就只能自行設計了。這就是本文要總結的第一個核心知識點:結構體。在C語言中結構體是對數據類型的無限擴展。
  • v01.12 鴻蒙內核源碼分析(雙向鍊表) | 誰是內核最重要結構體
    typedef struct LOS_DL_LIST {//雙向鍊表,內核最重要結構體 struct LOS_DL_LIST *pstPrev; /**< Current node's pointer to the previous node *///前驅節點(左手) struct
  • Go:有了 sync 為什麼還有 atomic?
    atomicGo 是一種擅長並發的語言,啟動新的 goroutine 就像輸入 「go」 一樣簡單。隨著你發現自己構建的系統越來越複雜,正確保護對共享資源的訪問以防止競爭條件變得極其重要。此類資源可能包括可即時更新的配置(例如功能標誌)、內部狀態(例如斷路器狀態)等。01 什麼是競態條件?
  • Go error 處理最佳實踐
    今天分享 go 語言 error 處理的最佳實踐,了解當前 error 的缺點、妥協以及使用時注意事項。文章內容較長,乾貨也多,建義收藏什麼是 error大家都知道 error[1] 是原始碼內嵌的接口類型。
  • 萬字長文:Go error 處理最佳實踐
    今天分享 go 語言 error 處理的最佳實踐,了解當前 error 的缺點、妥協以及使用時注意事項。文章內容較長,乾貨也多,建義收藏什麼是 error大家都知道 error[1] 是原始碼內嵌的接口類型。
  • 如何用C語言實現面向對象編程OOP?
    在 C_OOP 中貫徹了這一思想,C中有一種複雜的數據結構叫做struct。struct是C裡面的結構體。 如上圖假如我們要對鳥bird進行封裝,bird可能包括姓名、顏色、棲息地、重量、屬性等信息。 p.name = 「bird」;p.color = 『b』; //『b』 = black; 『g』 = greenp.addr = 『w』; p.weight = 175;p.other = 1; 繼承在常見用C語言實現繼承的機制中,多半是用結構體組合實現的,同樣利用struct,我們來創建一個Bird結構,同時繼承結構體Bird,如下: struct fBird
  • 兩款 go 開發實用工具
    是為了讓評論區的大佬介紹其他更好用的工具,解放我的雙手。順便問問,有沒有隻說話就能自動打完代碼的工具?JSON-To-Stuct這個工具可以把json格式的數據轉換成go的struct。比如你在對接第三方的時候,就不需要根據對方的接口一個個定義struct欄位。
  • Go 三款主流框架--Gin Beego Iris 選型對比
    / http和negroni-like處理程序通過iris.FromStd兼容針對任意Http請求錯誤 定義處理函數支持事務和回滾支持響應緩存使用簡單的函數嵌入資源並與go-bindataperson.Birthday)        }        c.String(200, "Success")    })    route.Run(":8085")}發送數據:Gin 輸出這 JSON、 XML、 YAML 三種格式非常方便,直接使用對用方法並賦值一個結構體給它就行了
  • 編程代碼:用C語言來實現下雪效果,這個冬天,雪花很美
    上面一共用了 5個頭文件 還是容易的代碼. string.h 主要用的是 memset 函數, 讓一段內存初始化,用0填充.對於time.h 主要是為了 初始化時間種子,方便每次運行都不一樣.*/struct screen {intfrate;// 也可以用 unsigned 結構int width; int height; char*pix;};創建了一個繪圖對象 struct screen 這裡 構建這個結構體的時候用了下面一個技巧
  • gorm 作者出品:Go 每日一庫之 copier
    高級特性方法賦值目標對象中的一些欄位,源對象中沒有,但是源對象有同名的方法。Employee結構有欄位DoubleAge,User中沒有,但是User有一個同名的方法,這時Copy調用user的DoubleAge方法為employee的DoubleAge賦值,得到 36。