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>

        深度剖析 Go 的 nil

        共 11005字,需瀏覽 23分鐘

         ·

        2021-04-14 00:51


        前幾天有小伙伴問(wèn)我說(shuō),golang 里面很多類型使用 nil 來(lái)賦值和做條件判斷,總是混淆記不住。你可能見(jiàn)過(guò):

        1. 很多文章和書會(huì)教你:Go 語(yǔ)言默認(rèn)定義的類型賦值會(huì)被 nil;
        2. error 返回值經(jīng)常用 return nil 的寫法;
        3. 多種類型都可以使用 if 是否 != nil;

        上面的事情在 Go 編程里隨處可見(jiàn),下面思考幾個(gè)問(wèn)題,看自己對(duì) nil 這個(gè)知識(shí)點(diǎn)是否做到了知其所以然

        1. nil 是一個(gè)關(guān)鍵字?還是類型?還是變量?
        2. 并非所有類型都跟 nil 有關(guān)系,有哪些類型可以使用 != nil 的語(yǔ)法?
        3. 這些不同的類型和 nil 打交道又有什么異同?
        4. 為什么有些復(fù)合結(jié)構(gòu)定義了變量還不夠,還必須要 make(Type) 才能使用 ?否則會(huì)出 panic;
        5. 很多書里講 slice 也要 make 之后才能用,但其實(shí)不必要,其實(shí) slice 只要定義了就能用。map 結(jié)構(gòu)卻光定義還不行,一定要 make(Type) 才能使用

        下面我們就這幾個(gè)思考題展開(kāi),剖析 nil 的秘密。


        Go 里面 nil 到底是什么?


        我們思考的第一個(gè)問(wèn)題是:nil 是一個(gè)關(guān)鍵字?還是類型?還是變量?

        答案自然是:變量。具體是什么樣的變量,我們可以點(diǎn)進(jìn)去 Go 的源碼看下:


        一窺 Go 官方定義和解釋

        // nil is a predeclared identifier representing the zero value for a
        // pointer, channel, func, interface, map, or slice type.
        var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

        // Type is here for the purposes of documentation only. It is a stand-in
        // for any Go type, but represents the same type for any given function
        // invocation.
        type Type int

        從類型定義得到兩個(gè)關(guān)鍵點(diǎn)

        1. nil 本質(zhì)上是一個(gè) Type 類型的變量而已;
        2. Type 類型僅僅是基于 int 定義出來(lái)的一個(gè)新類型;

        nil 官方的注釋中,我們可以得到一個(gè)重要信息:

        劃重點(diǎn)nil 適用于 指針函數(shù),interface,mapslice,channel 這 6 種類型。


        Go 和 C 的變量定義異同


        相同點(diǎn)

        Go 和 C 的變量定義回歸最本質(zhì)原理:分配變量指定大小的內(nèi)存,確定一個(gè)變量名稱。

        不同點(diǎn)

        • Go 分配內(nèi)存是置 0 分配的。置 0 分配的意思是:Go 確保分配出來(lái)的內(nèi)存塊里面是全 0 數(shù)據(jù);
        • C 默認(rèn)分配的內(nèi)存則僅僅是分配內(nèi)存,里面的數(shù)據(jù)不能做任何假設(shè),里面是未定義的數(shù)據(jù),可能是全 0 ,可能是全 1,可能是 0101 等;

        Go 置 0 分配的原理

        • 棧上變量的內(nèi)存編譯階段由編譯器就保證了置 0 分配,這種反匯編看下就知道了;
        • 堆上變量的內(nèi)存由 runtime 保證,可以仔細(xì)觀察下 mallocgc 這個(gè)函數(shù)參數(shù)有一個(gè) needzero 的參數(shù),用戶變量定義觸發(fā)的入口(比如 newobject 等等 )這個(gè)參數(shù)為 true,而該參數(shù)就是顯式指定置 0 分配的。
        func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
            // ...
        }

        思考一個(gè)小問(wèn)題:Go 既然所用的類型定義都是置 0 分配的,那為什么 mallocgc 需要 needzero 這么一個(gè)參數(shù)來(lái)控制呢?

        首先,Go 的類型定義一定確保是置 0 分配的,這個(gè)是 Go 語(yǔ)言給到 Go 程序員的語(yǔ)義。Go runtime 眾多的內(nèi)部的流程(對(duì) Go 程序員不感知的層面)是沒(méi)有這個(gè)規(guī)定的。其次,置 0 分配是有性能代價(jià)的,如果在確保語(yǔ)義的情況下,能不做自然是最好的。

        劃重點(diǎn):Go 的變量定義由語(yǔ)言層面確保置 0 分配,確保內(nèi)存塊全 0 數(shù)據(jù)。請(qǐng)記住這個(gè)最本質(zhì)的約定。


        怎么理解 nil


        通過(guò)上面,我們理解了幾個(gè)東西:

        1. Go 的類型定義僅比 C 多做了一件事,把分配的內(nèi)存塊置 0,而已;
        2. 能夠和 nil 值做判斷的,僅僅有 6 個(gè)類型。如果你用來(lái)其他類型來(lái)和 nil 比較,那么在編譯期間 typecheck 會(huì)報(bào)錯(cuò)檢查到會(huì)報(bào)錯(cuò);

        就筆者理解,nil 這個(gè)概念是更高一層的概念,在語(yǔ)言級(jí)別,而這個(gè)概念是由編譯器帶給你的。不是所有的類型都可以和 nil 進(jìn)行比較或者賦值,只有這 6 種類型的變量才能和 nil 值比較,因?yàn)檫@是編譯器決定的。

        同樣的,你不能賦值一個(gè) nil 變量給一個(gè)整型,原理也很簡(jiǎn)單,僅僅是編譯器不讓,就這么簡(jiǎn)單。

        所以,nil 其實(shí)更準(zhǔn)確的理解是一個(gè)觸發(fā)條件,編譯器看到和 nil 值比較的寫法,那么就要確認(rèn)類型在這 6 種類型以內(nèi),如果是賦值 nil,那么也要確認(rèn)在這 6 種類型以內(nèi),并且對(duì)應(yīng)的結(jié)構(gòu)內(nèi)存為全 0 數(shù)據(jù)。

        所以,記住這句話,nil 是編譯器識(shí)別行為的一個(gè)觸發(fā)點(diǎn)而已,看到這個(gè) nil 會(huì)觸發(fā)編譯器的一些特殊判斷和操作。


        和 nil 打交道的 6 大類型



        slice 類型


        變量定義

        創(chuàng)建 slice 的本質(zhì)上是 2 種:

        1. var 關(guān)鍵字定義;
        2. make 關(guān)鍵字創(chuàng)建;
        // 方式一
        var slice1 []byte
        var slice2 []byte = []byte{0x10x20x3}

        // 方式二
        var slice3 = make([]byte0)
        var slice4 = make([]byte3)

        首先,slice 變量本身占多少個(gè)字節(jié)?

        答案是:24 個(gè)字節(jié)。1 個(gè)指針字段,2 個(gè) 8 字節(jié)的整形字段。

        思考:varmake 這兩種方式有什么區(qū)別?

        • 第一種 var 的方式定義變量純粹真的是變量定義,如果逃逸分析之后,確認(rèn)可以分配在棧上,那就在棧上分配這 24 個(gè)字節(jié),如果逃逸到堆上去,那么調(diào)用 newobject 函數(shù)進(jìn)行類型分配。
        • 第二種 make 方式則略有不同,如果逃逸分析之后,確認(rèn)分配在棧上,那么也是直接在棧上分配 24 字節(jié),如果逃逸到堆上則會(huì)導(dǎo)致調(diào)用 makeslice 函數(shù)來(lái)分配變量。

        變量本身

        定義的變量本身分配了多少內(nèi)存?

        上面已經(jīng)說(shuō)過(guò)了,無(wú)論多大的 slice ,變量本身占用 24 字節(jié)。這 24 個(gè)字節(jié)其實(shí)是動(dòng)態(tài)數(shù)組的管理結(jié)構(gòu),如下:

        type slice struct {
         array unsafe.Pointer     // 管理的內(nèi)存塊首地址
         len   int                    // 動(dòng)態(tài)數(shù)組實(shí)際使用大小
         cap   int                    // 動(dòng)態(tài)數(shù)組內(nèi)存大小
        }

        該結(jié)構(gòu)體定義在 src/runtime/slice.go 里。

        劃重點(diǎn):我們看到無(wú)論是 var 聲明定義的 slice 變量,還是 make(xxx,num) 創(chuàng)建的 slice 變量,slice 管理結(jié)構(gòu)是已經(jīng)分配出來(lái)了的(也就是 struct slice 結(jié)構(gòu) )。

        所以, 對(duì)于 slice 來(lái)說(shuō),其實(shí)并不需要 make 創(chuàng)建的才能使用,直接用 var 定義出來(lái)的 slice 也能直接使用。如下:

        // 定義一個(gè) slice
        var slice1 []byte
        // 使用這個(gè) slice
        slice1 = append(slice1, 0x1)

        定義的時(shí)候,slice 結(jié)構(gòu)本身就已經(jīng)置 0 分配了,這個(gè) 24 字節(jié)的 slice 結(jié)構(gòu)就是管理動(dòng)態(tài)數(shù)組的核心。有這個(gè)在 append 函數(shù)就能正常處理 slice 變量。

        思考:append 又是怎么處理的呢?

        本質(zhì)是調(diào)用 runtime.growslice 函數(shù)來(lái)處理。

        nil 賦值

        如果把一個(gè)已經(jīng)存在的 slice 結(jié)構(gòu)賦值 nil ,會(huì)發(fā)生什么事情?

        var slice2 []byte = []byte{0x10x20x3}

        // slice 賦值 nil
        slice2 = nil

        發(fā)生什么事?

        事情在編譯期間就確定了,就是把 slice2 變量本身內(nèi)存塊置 0 ,也就是說(shuō) slice2 本身的 24 字節(jié)的內(nèi)存塊被置 0。

        nil 值判斷

        編譯器認(rèn)為 slice 做可以做 nil 判斷,那么什么樣的 slice 認(rèn)為是 nil 的?

        指針值為 0 的,也就是說(shuō)這個(gè)動(dòng)態(tài)數(shù)組沒(méi)有實(shí)際數(shù)據(jù)的時(shí)候。

        思考:僅判斷指針?對(duì) len 和 cap 兩個(gè)字段不做判斷嗎?

        只對(duì)首字段 array 做非 0 判斷,len,cap 字段不做判斷。

        如下:

        var a []byte = []byte{0x10x20x3}
        if a != nil {
        }

        對(duì)應(yīng)的部分匯編代碼如下:

        // 賦值 array 的值
        0x00000000004587cd <+93>: mov    %rax,0x20(%rsp)
        // 賦值 len 的值
        0x00000000004587d2 <+98>: movq   $0x3,0x28(%rsp)
        // 賦值 cap 的值
        0x00000000004587db <+107>: movq   $0x3,0x30(%rsp)
        // 判斷 slice 是否是 nil
        => 0x00000000004587e4 <+116>: test   %rax,%rax

        不信 Go 只判斷首字段?為了驗(yàn)證,自己思考下一下的程序的輸出:

        package main

        import (
         "unsafe"
        )

        type sliceType struct {
         pdata unsafe.Pointer
         len   int
         cap   int
        }

        func main() {
         var a []byte

         ((*sliceType)(unsafe.Pointer(&a))).len = 0x3
         ((*sliceType)(unsafe.Pointer(&a))).cap = 0x4

         if a != nil {
           println("not nil")
         } else {
           println("nil")
         }
        }

        答案是:輸出 nil。


        map 類型

        變量定義

        // 變量定義
        var m1 map[string]int
        // 定義 & 初始化
        var m2 = make(map[string]int)

        和 slice 類似,上面也是兩種差別的方式:

        • 第一種方式僅僅定義了 m1 變量本身;
        • 第二種方式則是分配 m2 的內(nèi)存,還會(huì)調(diào)用 makehmap 函數(shù)(不一定是這個(gè)函數(shù),要看逃逸分析的結(jié)果,如果是可以棧上分配的,會(huì)有一些優(yōu)化)來(lái)創(chuàng)建某個(gè)結(jié)構(gòu),并且把這個(gè)函數(shù)的返回值賦給 m2;

        變量本身

        map 的變量本身究竟是什么?比如上面的 m1,m2 ?

        m1, m2 變量本身是一個(gè)指針,內(nèi)存占用 8 字節(jié)。這個(gè)指針指向的結(jié)構(gòu)才大有來(lái)頭,指向一個(gè) struct hmap 結(jié)構(gòu)。

        type hmap struct {
         count     int // # live cells == size of map.  Must be first (used by len() builtin)
         flags     uint8
         B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
         noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
         hash0     uint32 // hash seed

         buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
         oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
         nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

         extra *mapextra // optional fields
        }

        所以,回到思考問(wèn)題:為什么 map 結(jié)構(gòu)卻光定義還不行,一定要 make(XXMap) 才能使用?

        因?yàn)椋?code style="padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">map 結(jié)構(gòu)的核心在于 struct hmap 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體是很大的一個(gè)結(jié)構(gòu)體。map 的操作核心都是基于這個(gè)結(jié)構(gòu)體之上的。而 var 定義一個(gè) map 結(jié)構(gòu)的時(shí)候,只是分配了一個(gè) 8 字節(jié)的指針,只有調(diào)用 make 的時(shí)候,才觸發(fā)調(diào)用 makemap ,在這個(gè)函數(shù)里面分配出一個(gè)龐大的 struct hmap 結(jié)構(gòu)體。

        nil 賦值

        如果把一個(gè) map 變量賦值 nil 那就很容易理解了,僅僅是把這個(gè)變量本身置 0 而已,也就是這個(gè)指針變量置 0 ,hmap 結(jié)構(gòu)體本身是不會(huì)動(dòng)的。

        當(dāng)然考慮垃圾回收的話,如果這個(gè) m1 是唯一的指向這個(gè) hmap 結(jié)構(gòu),那么 m1 賦值 nil 之后,那么這個(gè) hmap 結(jié)構(gòu)體之后就可能被回收。

        nil 值判斷

        搞懂了變量本身和管理結(jié)構(gòu)的區(qū)別就很簡(jiǎn)單了,這里的 nil 值判斷也僅僅是針對(duì)變量本身的判斷,只要是非 0 指針,那么就是非 nil 。也就是說(shuō) m1 只要是一個(gè)非 0 的指針,就不會(huì)是非nil 的。

        package main

        func main() {
         var m1 map[string]int
         var m2 = make(map[string]int)
         if m1 != nil {
          println("m1 not nil")
         } else {
          println("m1 nil")
         }
         if m2 != nil {
          println("m2 not nil")
         } else {
          println("m2 nil")
         }
        }

        如上示例程序,m1 是一個(gè) 0 指針,m2 被賦值了的。


        interface 類型

        變量定義

        // 定義一個(gè)接口
        type Reader interface {
         Read(p []byte) (n int, err error)
        }

        // 定義一個(gè)接口變量
        var reader Reader
        // 或者一個(gè)空接口
        var empty interface{}

        變量本身

        interface 稍微有點(diǎn)特殊,有兩種對(duì)應(yīng)的結(jié)構(gòu)體,如下:

        type iface struct {
            tab  *itab
            data unsafe.Pointer
        }

        type eface struct {
            _type *_type
            data  unsafe.Pointer
        }

        其中,iface 就是通常定義的 interface 類型,eface 則是通常人們常說(shuō)的空接口 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)。

        不管內(nèi)部怎么樣,這兩個(gè)結(jié)構(gòu)體占用內(nèi)存是一樣的,都是一個(gè)正常的指針類型和一個(gè)無(wú)類型的指針類型( Pointer ),總共占用 16 個(gè)字節(jié)。

        也就是說(shuō),如果你聲明定義一個(gè) interface 類型,無(wú)論是空接口,還是具體的接口類型,都只是分配了一個(gè) 16 字節(jié)的內(nèi)存塊給你,注意是置 0 分配哦。

        nil 賦值

        和上面類似,如果對(duì)一個(gè) interface 變量賦值 nil 的話,發(fā)生的事情也僅僅是把變量本身這 16 個(gè)字節(jié)的內(nèi)存塊置 0 而已。

        nil 值判斷

        判斷 interface 是否是 nil ?這個(gè)跟 slice 類似,也僅僅是判斷首字段(指針類型)是否為 0 即可。因?yàn)槿绻浅跏蓟^(guò)的,首字段一定是非 0 的。


        channel 類型

        變量定義

        // 變量本身定義
        var c1 chan struct{}
        // 變量定義和初始化
        var c2 = make(chan struct{})

        區(qū)別:

        • 第一種方式僅僅定義了 c1 變量本身;
        • 第二種方式則是分配 c2 的內(nèi)存,還會(huì)調(diào)用 makechan 函數(shù)來(lái)創(chuàng)建某個(gè)結(jié)構(gòu),并且把這個(gè)函數(shù)的返回值賦給 c2;

        變量本身

        定義的 channel 變量本身是什么一個(gè)表現(xiàn)?

        答案是:一個(gè) 8 字節(jié)的指針而已,意圖指向一個(gè) channel 管理結(jié)構(gòu),也就是 struct hchan 的指針。

        程序員定義的 channel 變量本身內(nèi)存僅僅是一個(gè)指針,channel  所有的邏輯都在 hchan 這個(gè)管理結(jié)構(gòu)體上,所以,channel  也是必須 make(chan Xtype) 之后才能使用,就是這個(gè)道理。

        nil 賦值

        賦值 nil 之后,僅僅是把這 8 字節(jié)的指針置 0 。

        nil 值判斷

        簡(jiǎn)單,僅僅是判斷這 channel 指針是否非 0 而已。


        指針 類型


        指針和函數(shù)類型比較好理解,因?yàn)橹暗?4 種類型 slice,mapchannel,interface 是復(fù)合結(jié)構(gòu)。

        指針本身來(lái)說(shuō)也只是一個(gè) 8 字節(jié)的整型,函數(shù)變量類型則本身就是個(gè)指針。

        變量定義

        var ptr *int

        變量本身

        變量本身就是一個(gè) 8 字節(jié)的內(nèi)存塊,這個(gè)沒(méi)啥好講的,因?yàn)橹羔樁疾皇菑?fù)合類型。

        nil 賦值

        ptr = nil

        這 8 字節(jié)的指針置 0。

        nil 值判斷

        判斷這 8 字節(jié)的指針是否為 0 。


        函數(shù) 類型

        變量定義

        var f func(int) error

        變量本身

        變量本身是一個(gè) 8 字節(jié)的指針。

        nil 賦值

        本身就是指針,只不過(guò)指向的是函數(shù)而已。所以賦值也僅僅是這 8 字節(jié)置 0 。

        nil 值判斷

        判斷這 8 字節(jié)是否為 0 。


        總結(jié)


        下面總結(jié)一些上述分享:

        1. 請(qǐng)撇開(kāi)死記硬背的語(yǔ)法和玄學(xué),變量?jī)H僅是綁定到一個(gè)指定內(nèi)存塊的名字;
        2. Go 從語(yǔ)言層面對(duì)程序員做了承諾,變量定義分配的內(nèi)存一定是置 0 分配的;
        3. 并不是所有的類型能夠賦值 nil,并且和 nil 進(jìn)行對(duì)比判斷。只有 slice、mapchannel、interface、指針、函數(shù) 這 6 種類型;
        4. 不要把 nil 理解成一個(gè)特殊的值,而要理解成一個(gè)觸發(fā)條件,編譯器識(shí)別到代碼里有 nil 之后,會(huì)對(duì)應(yīng)做出處理和判斷;
        5. channelmap 類型的變量必須要 make 才能使用的原因(否則會(huì)出現(xiàn)空指針的 panic )在于 var 定義的變量?jī)H僅是分配了一個(gè)指向 hchanhmap 的指針變量而已,并且還是置 0 分配的。真正的管理結(jié)構(gòu)只有 make 調(diào)用才能分配出來(lái),對(duì)應(yīng)的函數(shù)分別是 makechanmakemap 等;
        6. slice 變量為什么 var 就能用是因?yàn)?struct slice 核心結(jié)構(gòu)是定義的時(shí)候就分配出來(lái)了;
        7. 以上 6 種變量賦值 nil 的行為都是把變量本身置 0 ,僅此而已。slice 的 24 字節(jié)管理結(jié)構(gòu),map 的  8 字節(jié)指針,channel 的 8 字節(jié)指針,interface 的 16 字節(jié),8 字節(jié)指針和函數(shù)指針也是如此;
        8. 以上 6 種類型和 nil 進(jìn)行比較判斷本質(zhì)上都是和變量本身做判斷,slice 是判斷管理結(jié)構(gòu)的第一個(gè)指針字段,map,channel 本身就是指針,interface 也是判斷管理結(jié)構(gòu)的第一個(gè)指針字段,指針和函數(shù)變量本身就是指針;

        后記


        推薦使用 gdb 進(jìn)行對(duì)上面的 demo 程序進(jìn)行調(diào)試,加深自己理解。重點(diǎn)關(guān)注內(nèi)存分配和內(nèi)部代碼的生成(反匯編),比如類似 makechan 這樣的函數(shù),如果你不調(diào)試,你根本不會(huì)知道竟然還有這個(gè),我明明沒(méi)有寫過(guò)這函數(shù)呀?這個(gè)是編譯器幫你生成的。


        推薦閱讀


        福利

        我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

        瀏覽 28
        點(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>
            午夜免费爱爱视频 | av777777 | 国产精品福利久久 | 日屄在线观看 | 日韩免费一区二区三区 | 少妇又色又爽 | 高清国产毛片 | 操人无码黄色视频免费 | 波多野结衣一区二区 | 中文在线а√在线8 |