<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深度剖析 Go 的 nil

          共 11005字,需瀏覽 23分鐘

           ·

          2021-04-14 00:51


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

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

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

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

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


          Go 里面 nil 到底是什么?


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

          答案自然是:變量。具體是什么樣的變量,我們可以點(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

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

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

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

          劃重點(diǎn)nil 適用于 指針函數(shù)interfacemapslicechannel 這 6 種類(lèi)型。


          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 既然所用的類(lèi)型定義都是置 0 分配的,那為什么 mallocgc 需要 needzero 這么一個(gè)參數(shù)來(lái)控制呢?

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

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

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

          所以,nil 其實(shí)更準(zhǔn)確的理解是一個(gè)觸發(fā)條件,編譯器看到和 nil 值比較的寫(xiě)法,那么就要確認(rèn)類(lèi)型在這 6 種類(lèi)型以內(nèi),如果是賦值 nil,那么也要確認(rèn)在這 6 種類(lèi)型以內(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 大類(lèi)型



          slice 類(lèi)型


          變量定義

          創(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)行類(lèi)型分配。
          • 第二種 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 類(lèi)型

          變量定義

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

          和 slice 類(lèi)似,上面也是兩種差別的方式:

          • 第一種方式僅僅定義了 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 的變量本身究竟是什么?比如上面的 m1m2 ?

          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 類(lèi)型

          變量定義

          // 定義一個(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 類(lèi)型,eface 則是通常人們常說(shuō)的空接口 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)。

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

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

          nil 賦值

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

          nil 值判斷

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


          channel 類(lèi)型

          變量定義

          // 變量本身定義
          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 而已。


          指針 類(lèi)型


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

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

          變量定義

          var ptr *int

          變量本身

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

          nil 賦值

          ptr = nil

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

          nil 值判斷

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


          函數(shù) 類(lèi)型

          變量定義

          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. 并不是所有的類(lèi)型能夠賦值 nil,并且和 nil 進(jìn)行對(duì)比判斷。只有 slicemapchannelinterface、指針、函數(shù) 這 6 種類(lèi)型;
          4. 不要把 nil 理解成一個(gè)特殊的值,而要理解成一個(gè)觸發(fā)條件,編譯器識(shí)別到代碼里有 nil 之后,會(huì)對(duì)應(yīng)做出處理和判斷;
          5. channelmap 類(lèi)型的變量必須要 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 種類(lèi)型和 nil 進(jìn)行比較判斷本質(zhì)上都是和變量本身做判斷,slice 是判斷管理結(jié)構(gòu)的第一個(gè)指針字段mapchannel 本身就是指針,interface 也是判斷管理結(jié)構(gòu)的第一個(gè)指針字段,指針和函數(shù)變量本身就是指針;

          后記


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


          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(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)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  又大又爽| 色色中文网 | 国产一区精品视频 | 欧美干在线观看 | 插插视频网站 |