1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        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)集合和通道。
        參考資料
        [1]

        example-1: https://go.dev/play/p/WNxfXviET_i

        [2]

        example-2: https://go.dev/play/p/HcxlywljovS


        瀏覽 98
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一区二区三区在线播放 | 蘑菇视频红色logo | 18禁无码永久免费网站大全 | www.超碰在线.com | 四虎婷婷| 欧美日韩一区不卡 | 91蜜臀| 东京热日韩无码 | 久操免费| free艳丽少妇pics |