<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 之 slice

          共 5821字,需瀏覽 12分鐘

           ·

          2021-05-20 01:13

          簡介

          切片(slice)是 Go 語言提供的一種數(shù)據(jù)結構,使用非常簡單、便捷。但是由于實現(xiàn)層面的原因,切片也經(jīng)常會產(chǎn)生讓人疑惑的結果。掌握切片的底層結構和原理,可以避免很多常見的使用誤區(qū)。

          底層結構

          切片結構定義在源碼runtime包下的 slice.go 文件中:

          // src/runtime/slice.go
          type slice struct {
            array unsafe.Pointer
            len int
            cap int
          }
          • array:一個指針,指向底層存儲數(shù)據(jù)的數(shù)組
          • len:切片的長度,在代碼中我們可以使用len()函數(shù)獲取這個值
          • cap:切片的容量,即在不擴容的情況下,最多能容納多少元素。在代碼中我們可以使用cap()函數(shù)獲取這個值

          我們可以通過下面的代碼輸出切片的底層結構:

          type slice struct {
            array unsafe.Pointer
            len   int
            cap   int
          }

          func printSlice() {
            s := make([]uint32110)
            fmt.Printf("%#v\n", *(*slice)(unsafe.Pointer(&s)))
          }

          func main() {
            printSlice()
          }

          運行輸出:

          main.slice{array:(unsafe.Pointer)(0xc0000d6030), len:1, cap:10}

          這里注意一個細節(jié),由于runtime.slice結構是非導出的,我們不能直接使用。所以我在代碼中手動定義了一個slice結構體,字段與runtime.slice結構相同。

          我們結合切片的底層結構,先回顧一下切片的基礎知識,然后再逐一看看切片的常見問題。

          基礎知識

          創(chuàng)建切片

          創(chuàng)建切片有 4 種方式:

          1. var

          var聲明切片類型的變量,這時切片值為nil。

          var s []uint32

          這種方式創(chuàng)建的切片,array字段為空指針,lencap字段都等于 0。

          1. 切片字面量

          使用切片字面量將所有元素都列舉出來,這時切片長度和容量都等于指定元素的個數(shù)。

          s := []uint32{123}

          創(chuàng)建之后s的底層結構如下:

          lencap字段都等于 3。

          1. make

          使用make創(chuàng)建,可以指定長度和容量。格式為make([]type, len[, cap]),可以只指定長度,也可以長度容量同時指定:

          s1 := make([]uint32)
          s2 := make([]uint321)
          s3 := make([]uint32110)
          1. 切片操作符

          使用切片操作符可以從現(xiàn)有的切片或數(shù)組中切取一部分,創(chuàng)建一個新的切片。切片操作符格式為[low:high],例如:

          var arr [10]uint32
          s1 := arr[0:5]
          s2 := arr[:5]
          s3 := arr[5:]
          s4 := arr[:]

          區(qū)間是左開右閉的,即[low, high),包括索引low,不包括high。切取生成的切片長度為high-low。

          另外lowhigh都有默認值。low默認為 0,high默認為原切片或數(shù)組的長度。它們都可以省略,省略時,相當于取默認值。

          使用這種方式創(chuàng)建的切片底層共享相同的數(shù)據(jù)空間,在進行切片操作時可能會造成數(shù)據(jù)覆蓋,要格外小心。

          添加元素

          可以使用append()函數(shù)向切片中添加元素,可以一次添加 0 個或多個元素。如果剩余空間(即cap-len)足夠存放元素則直接將元素添加到后面,然后增加字段len的值即可。反之,則需要擴容,分配一個更大的數(shù)組空間,將舊數(shù)組中的元素復制過去,再執(zhí)行添加操作。

          package main

          import "fmt"

          func main() {
            s := make([]uint3204)

            s = append(s, 123)
            fmt.Println(len(s), cap(s)) // 3 4

            s = append(s, 456)
            fmt.Println(len(s), cap(s)) // 6 8
          }

          你不知道的 slice

          1. 空切片等于nil嗎?

          下面代碼的輸出什么?

          func main() {
            var s1 []uint32
            s2 := make([]uint320)

            fmt.Println(s1 == nil)
            fmt.Println(s2 == nil)
            fmt.Println("nil slice:"len(s1), cap(s1))
            fmt.Println("cap slice:"len(s2), cap(s2))
          }

          分析:

          首先s1s2的長度和容量都為 0,這很好理解。比較切片與nil是否相等,實際上要檢查slice結構中的array字段是否是空指針。顯然s1 == nil返回trues2 == nil返回false。盡管s2長度為 0,但是make()為它分配了空間。所以,一般定義長度為 0 的切片使用var的形式。

          1. 傳值還是傳引用?

          下面代碼的輸出什么?

          func main() {
            s1 := []uint32{123}
            s2 := append(s1, 4)

            fmt.Println(s1)
            fmt.Println(s2)
          }

          分析:

          為什么append()函數(shù)要有返回值?因為我們將切片傳遞給append()時,其實傳入的是runtime.slice結構。這個結構是按值傳遞的,所以函數(shù)內(nèi)部對array/len/cap這幾個字段的修改都不影響外面的切片結構。上面代碼中,執(zhí)行append()之后s1lencap保持不變,故輸出為:

          [1 2 3]
          [1 2 3 4]

          所以我們調用append()要寫成s = append(s, elem)這種形式,將返回值賦值給原切片,從而覆寫array/len/cap這幾個字段的值。

          初學者還可能會犯忽略append()返回值的錯誤:

          append(s, elem)

          這就更加大錯特錯了。添加的元素將會丟失,以為函數(shù)外切片的內(nèi)部字段都沒有變化。

          我們可以看到,雖說切片是按引用傳遞的,但是實際上傳遞的是結構runtime.slice的值。只是對現(xiàn)有元素的修改會反應到函數(shù)外,因為底層數(shù)組空間是共用的。

          1. 切片的擴容策略

          下面代碼的輸出是什么?

          func main() {
            var s1 []uint32
            s1 = append(s1, 123)
            s2 := append(s1, 4)
            fmt.Println(&s1[0] == &s2[0])
          }

          這涉及到切片的擴容策略。擴容時,若:

          • 當前容量小于 1024,則將容量擴大為原來的 2 倍;
          • 當前容量大于等于 1024,則將容量逐次增加原來的 0.25 倍,直到滿足所需容量。

          我翻看了 Go1.16 版本runtime/slice.go中擴容相關的源碼,在執(zhí)行上面規(guī)則后還會根據(jù)切片元素的大小和計算機位數(shù)進行相應的調整。整個過程比較復雜,感興趣可以自行去研究。

          我們只需要知道一開始容量較小,擴大為 2 倍,降低后續(xù)因添加元素導致擴容的頻次。容量擴張到一定程度時,再按照 2 倍來擴容會造成比較大的浪費。

          上面例子中執(zhí)行s1 = append(s1, 1, 2, 3)后,容量會擴大為 4。再執(zhí)行s2 := append(s1, 4)由于有足夠的空間,s2底層的數(shù)組不會改變。所以s1s2第一個元素的地址相同。

          1. 切片操作符可以切取字符串

          切片操作符可以切取字符串,但是與切取切片和數(shù)組不同。切取字符串返回的是字符串,而非切片。因為字符串是不可變的,如果返回切片。而切片和字符串共享底層數(shù)據(jù),就可以通過切片修改字符串了。

          func main() {
            str := "hello, world"
            fmt.Println(str[:5])
          }

          輸出 hello。

          1. 切片底層數(shù)據(jù)共享

          下面代碼的輸出是什么?

          func main() {
            array := [10]uint32{12345}
            s1 := array[:5]

            s2 := s1[5:10]
            fmt.Println(s2)

            s1 = append(s1, 6)
            fmt.Println(s1)
            fmt.Println(s2)
          }

          分析:

          首先注意到s2 := s1[5:10]上界 10 已經(jīng)大于切片s1的長度了。要記住,使用切片操作符切取切片時,上界是切片的容量,而非長度。這時兩個切片的底層結構有重疊,如下圖:

          這時輸出s2為:

          [00000]

          然后向切片s1中添加元素 6,這時結構如下圖,其中切片s1s2共享元素 6:

          這時輸出的s1s2為:

          [1, 2, 3, 4, 5, 6]
          [6, 0, 0, 0, 0]

          可以看到由于切片底層數(shù)據(jù)共享可能造成修改一個切片會導致其他切片也跟著修改。這有時會造成難以調試的 BUG。為了一定程度上緩解這個問題,Go 1.2 版本中提供了一個擴展切片操作符:[low:high:max],用來限制新切片的容量。使用這種方式產(chǎn)生的切片容量為max-low

          func main() {
            array := [10]uint32{12345}
            s1 := array[:5:5]

            s2 := array[5:10:10]
            fmt.Println(s2)

            s1 = append(s1, 6)
            fmt.Println(s1)
            fmt.Println(s2)
          }

          執(zhí)行s1 := array[:5:5]我們限定了s1的容量為 5,這時結構如下圖所示:

          執(zhí)行s1 = append(s1, 6)時,發(fā)現(xiàn)沒有空閑容量了(因為len == cap == 5),重新創(chuàng)建一個底層數(shù)組再執(zhí)行添加。這時結構如下圖,s1s2互不干擾:

          總結

          了解了切片的底層數(shù)據(jù)結構,知道了切片傳遞的是結構runtime.slice的值,我們就能解決 90% 以上的切片問題。再結合圖形可以很直觀的看到切片底層數(shù)據(jù)是如何操作的。

          這個系列的名字是我仿造《你不知道的 JavaScript》起的??。

          參考

          1. 《Go 專家編程》,豆瓣鏈接:https://book.douban.com/subject/35144587/
          2. 你不知道的Go GitHub:https://github.com/darjun/you-dont-know-go


          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲?。贿€可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  日韩成人免费 | 免费毛片十八 | 日韩拍拍拍 | www啪啪 | 在线中文字幕777 |