Go資料結構系列之 Array and Alice

Remember發表於2020-12-12

圖片

概述

在使用 Go 開發的時候,陣列和切片經常被使用到,這篇文章來簡單聊聊吧。

陣列array

在 Go 中,有兩種方式可以初始化陣列


func main() {
  userId := [3]int{1, 2, 3}
  userName := [...]string{"wqq", "curry", "joke"}
}

一種是顯式的定義陣列的大小,另一種通過 […] 宣告陣列,Go 會在編譯期間推匯出陣列的大小。

既然使用了陣列,少不了遍歷,在 Go 中遍歷陣列一般也就兩種方式。


func main() {
  userIds := [3]int{1, 2, 3}
  names := [...]string{"wqq", "curry", "joke"}

  for i := 0; i < len(names); i++ {
    fmt.Printf("user id is:%v,user name is:%v\n", userIds[i], names[i])
  }

  for index, item := range names {
    fmt.Printf("user id is:%v,user name is:%v\n", userIds[index], item)
  }
}

第一種就是你所認知的 for 迴圈。第二種可以使用 for/range 表示式,該表示式返回兩個值,第一個值是索引,第二個值對應此索引的元素值。range 不單單能遍歷陣列,還能遍歷 slice、map、channel 等集合結構。當然這些不在這篇文章的討論範圍內。

切片slice

切片本質上是動態陣列,它的底層包含了對陣列的引用。切片的長度是動態的,可以隨意的對其進行 append 操作,在使用的過程中,如果容量不足,會自動進行擴容操作。我們可以從原始碼看看 slice 的結構。原始碼位於 src/runtime/slice.go ,更多底層知識可以自行檢視原始碼。


type slice struct {
  array unsafe.Pointer // 底層陣列的指標位置
  len   int // 切片當前長度
  cap   int //容量,當容量不夠時,會觸發動態擴容的機制
}

同理,初始化 slice 的方式也是多樣的。

  • 使用 make 關鍵字

  • 和陣列一樣,使用字面量初始化

  • 通過下標的方式獲取陣列或者切片的一部分,生成 slice

func main() {
  // 字面量初始化
  userIds1 := []int{1, 2, 3}

  // make初始化slice的長度為5,容量為10
  userIds2 := make([]int, 510)

  // 通過下標的方式獲取陣列的一部分作為alice
  userArray := [5]string{"curry", "wqq", "lisa", "tony", "james"}

  // 獲取從索引下標0開始,到下標3(不包括3)
  user := userArray[0:3]
  fmt.Printf("userIds1:%v,userIds2:%v,userSlice:%v\n", userIds1, userIds2, user)
}

這裡就拿 make 初始化切片進行說明。

圖片

:圖片來源 《Go程式設計專家》

這段初始化操作表示 slice 的長度是 5,容量是 10,array 欄位儲存的是引用陣列的指標位置。因為長度是 5,我們可以使用下標 0-4 來操作此 slice。同時容量是 10,所以後續向 slice 新增新資料暫時不需要重新分配新記憶體。

那陣列和切片有什麼關聯呢?

我們看看通過下標的方式獲取陣列資料,初始化切片的一種形式。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}

  // 獲取從索引下標0開始,到下標3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}

我們用陣列建立了 userSlice 的切片,此時 userSlice 將和 userArray 共用一部分記憶體。因此在修改 userSlice 索引 0 處的值時,操作的是同一塊陣列記憶體地址,從結果中可以看出生效了。

圖片

然後我們開始往 userSlice 切片新增元素。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 獲取從索引下標0開始,到下標3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

}

檢視輸出結果:

圖片

可以看到,再向 userSlice 增加一個元素後,列印結果,陣列和切片值一樣,操作之後 userSlice 的 len 是 4,陣列的長度也是 4。操作 append 後 userSlice 底層陣列和 userArray 指向的還是同一個記憶體地址,並不需要發生擴容。

這時候,userSlice 所引用的底層陣列已經滿了(底層陣列的長度是4),我們繼續向 userSlice 增加元素。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 獲取從索引下標0開始,到下標3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test2")
  fmt.Printf("userArray:%v,user:%v\n", userArray, userSlice)
 }

檢視輸出結果:

圖片

可以看到,userArray 的元素未變,因為這時候 userSlice 切片的長度已經大於原指向的陣列的長度了, userSlice 發生了擴容。

我們可以做個實驗測試一下,我們修改陣列 userArray 範圍內的 userSlice 元素的值,檢視陣列的資料是否會跟著改變。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 獲取從索引下標0開始,到下標3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test2")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
// 改變索引0處的值
  userSlice[0] = "only one"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}

圖片

最後一行已經說明了一切。此時的 userSlice 發生了擴容,不再和 userArray 共用原陣列空間了。因此對 userSlice 的改動不會影響到 userArray。

關於擴容

前面提到在向切片新增新元素時如果此時切片的容量不足,會自動發生擴容。所謂擴容,也就是為當前切片生成新的一塊記憶體空間,然後根據一定規則,將原切片的元素全部拷貝到新的地址。擴容的規則在 src/runtime/slice.go 裡的 growslice 方法。

圖片

這裡擷取了此方法中關於擴容規則的程式碼。

  • 如果期望的新容量 (cap) 大於當前容量的兩倍,那麼就直接使用期望的容量

  • 如果當前切片的長度 (len) 小於 1024,那麼把當前容量翻倍

  • 如果當前切片的長度(len) 大於等於 1024,那麼每次把當前容量增加 1/4,直到新容量值大於期望的的容量。

其實要寫下去還有很多東西,比如,sliceCopy、底層編譯邏輯……,有些東西我也沒看過,學習的最好方式還是自己動手然後輸出。

參考資料:

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

相關文章