GO 中 slice 的實現原理

小魔童哪吒發表於2021-06-19

上次我們分享的字串相關的內容我們回顧一下

  • 分享了字串具體是啥
  • GO 中字串的特性,為什麼不能被修改
  • 字串 GO 原始碼是如何構建的 ,原始碼檔案在 src/runtime/ 下的 string.go
  • 字串 和 []byte 的由來和應用場景
  • 字串與 []byte 相互轉換

要是對GO 對 字串 的編碼還有點興趣的話, 歡迎檢視文章 GO 中 string 的實現原理

slice 是什麼?

有沒有覺得很熟悉,上次分享的 string 型別 對應的資料結構 的前兩個引數 與 切片的資料結構的前兩個引數是一樣的

看看GO 的 src/runtime/ 下的 slice.go 原始碼,我們可以找到 slice的資料結構

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

// unsafe.Pointer 型別如下
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
//    - A pointer value of any type can be converted to a Pointer.
//    - A Pointer can be converted to a pointer value of any type.
//    - A uintptr can be converted to a Pointer.
//    - A Pointer can be converted to a uintptr.
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.
type Pointer *ArbitraryType

切片GO 的一種資料型別 , 是對陣列的一個連續片段的引用

切片的底層結構是一個結構體,對應有三個引數

  • array

是一個unsafe.Pointer指標,指向一個具體的底層陣列

  • len

指的是切片的長度

  • cap

指的是切片的容量

有沒有覺得,切片和我們瞭解的陣列好像是一樣的,但是好像又不一樣

slice 和 陣列的區別是啥?

大概有如下幾個區別

  • 陣列是複製傳遞的,而切片是引用傳遞的

在GO 裡面,傳遞陣列,是通過拷貝的方式

傳遞切片是通過引用的方式,這裡說的引用,指的是 切片資料結構中array欄位,其餘欄位預設是值傳遞

  • 陣列是相同型別的長度固定的序列

陣列是相同型別的,一組記憶體空間連續的資料,他的每一個元素的資料型別都是一樣的,且陣列的長度一開始就確定好了,且不能做變動

  • 切片是一個結構,是一個資料物件,且物件裡面有 3 個引數

切片是引用型別,切片的長度是不固定的,可擴充套件的,GO 裡面操作切片真的是香

當然,切片也是離不開陣列的,因為他的array指標就是指向的一個底層陣列,這個底層陣列,對使用者是不可見的

當使用切片的時候,陣列容量不夠的時候,這個底層陣列會自動重新分配,生成一個新的 切片注意,這裡是生成一個新的切片

如何建立 slice

建立一個新的切片有如下幾種方式:

  • 使用make 方法建立 新的切片
  • 使用陣列賦值的方式建立新的切片

使用make 方法建立 新的切片

新建一個 len 為 4,cap 為7 的切片:

func main(){
   mySlice := make([]int,4,7)

   // 此處的遍歷 長度是 len 的長度
   for _,v :=range mySlice{
      fmt.Printf("%v",v)
   }
}

上述程式碼執行結果為

0000

為什麼不是 7 個 0,而是4 個

這裡要注意了:

此處遍歷遍歷切片的長度是 切片的 len 值, 而不是切片的容量 cap

使用陣列賦值的方式建立新的切片

  • 建立一個 長度 為 8,資料型別為 int 的陣列
  • 陣列的第5個元素和第6個元素複製給到新的切片

func main(){

   arr := [8]int{}

   mySlice := arr[4:6]

   fmt.Println("len == ", len(mySlice))
   fmt.Println("cap == ", cap(mySlice))

   // 此處的遍歷 長度是 len 的長度
   for _,v :=range mySlice{
      fmt.Printf("%v",v)
   }
}

上述程式碼執行結果為

len ==  2
cap ==  4
00

根據程式碼執行情況,列印出 00,大家應該不會覺得奇怪

可是為什麼 cap 等於 4?

原因如下:

陣列的索引是從 0 開始的

上述程式碼 arr[4:6] 指的是將陣列的下標為 4 開始的位置,下標為 6 的為結束位置,這裡是不包含6自己的

根據 GO 中切片的原理,用陣列複製給到切片的時候,若複製的陣列元素後面還有內容的話,則後面的內容都作為切片的預留記憶體

即得到上述的結果,len == 2, cap == 4

不過這裡還是要注意,切片元素對應的地址,還是這個陣列元素對應的地址,使用的時候需要小心

slice 擴容原理是什麼?

我們就來模擬一下

  • 新建一個 長度為 4 ,容量為 4 的切片
  • 向切片中新增一個元素
  • 列印最終切片的詳細情況

func main(){

   mySlice := make([]int,4,4)
   mySlice[0] = 3
   mySlice[1] = 6
   mySlice[2] = 7
   mySlice[3] = 8

   fmt.Printf("ptr == %p\n", &mySlice)
   fmt.Println("len == ", len(mySlice))
   fmt.Println("cap == ", cap(mySlice))

   // 此處的遍歷 長度是 len 的長度
   for _,v :=range mySlice{
      fmt.Printf("%v ",v)
   }

   fmt.Println("")


   mySlice = append(mySlice,5)

   fmt.Printf("new_ptr == %p\n", &mySlice)
   fmt.Println("new_len == ", len(mySlice))
   fmt.Println("new_cap == ", cap(mySlice))

   // 此處的遍歷 長度是 len 的長度
   for _,v :=range mySlice{
      fmt.Printf("%v ",v)
   }
}

執行上述程式碼,有如下效果:

ptr == 0x12004110
len ==  4
cap ==  4
3 6 7 8
new_ptr == 0x12004110
new_len ==  5
new_cap ==  8
3 6 7 8 5

根據案例,相信大家或多或少心裡有點感覺了吧

向一個容量為 4 且長度為 4 的切片新增元素,我們發現切片的容量變成了 8

我們來看看切片擴容的規則是這樣的:

  • 如果原來的切片容量小於1024

那麼新的切片容量就會擴充套件成原來的 2 倍

  • 如果原切片容量大於等於1024

那麼新的切片容量就會擴充套件成為原來的1.25倍

我們再來梳理一下上述擴容原理的步驟是咋弄的

上述切片擴容,大致分為如下 2 種情況:

  • 新增的元素,加入到切片中,若原切片容量夠

那麼就直接新增元素,且切片的len ++ ,此處的新增可不是直接賦值,可是使用 append函式的方式,例如

func main(){

    mys := make([]int,3,5)
    fmt.Println("len == ", len(mys))
    fmt.Println("cap == ", cap(mys))

    mys[0] = 1
    mys[1] = 1
    mys[2] = 1
    // mys[3] = 2   會程式崩潰
     mys = append(mys,2)

    fmt.Println("len == ", len(mys))
    fmt.Println("cap == ", cap(mys))
    for _,v :=range mys{
        fmt.Printf("%v",v)
    }
}
  • 若原切片容量不夠,則先將切片擴容,再將原切片資料追加到新的切片中

簡單說一下空切片和 nil 切片

平時我們在使用JSON 序列化的時候,明明切片為空

為什麼有的 JSON 輸出是[] , 有的 JSON 輸出是 null

我們來看看這個例子

func main(){
   // 是一個空物件
   var mys1 []int
   // 是一個物件,物件裡面是一個切片,這個切片沒有元素
   var mys2 = []int{}

   json1, _ := json.Marshal(mys1)
   json2, _ := json.Marshal(mys2)

   fmt.Println(string(json1))
   fmt.Println(string(json2))
}

執行結果為

null
[]

原因是這樣的:

  • mys1 是一個空物件
  • mys2 不是一個空物件,是一個正常的物件,但是物件裡面的為空

總結

  • 分享了切片是什麼
  • 切片和陣列的區別
  • 切片的資料結構
  • 切片的擴容原理
  • 空切片 和 nil 切片的區別

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,下一次 GO 中 map 的實現原理分享

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章