快速理解Go陣列和切片的內部實現原理

RyuGou發表於2018-08-12

很多人對Go語言的arrayslice傻傻分不清楚,今天我們就從底層出發,來聊聊它倆到底有什麼區別。

陣列

幾乎所有計算機語言,陣列的實現都是相似的:一段連續的記憶體,Go語言也一樣,Go語言的陣列底層實現就是一段連續的記憶體空間。每個元素有唯一一個索引(或者叫下標)來訪問。如下圖所示,下圖是[5]int{1:10, 2:20}陣列的內部實現邏輯圖:

image

由於記憶體連續,CPU很容易計算索引(即陣列的下標),可以快速迭代陣列裡的所有元素。 Go語言的陣列不同於C語言或者其他語言的陣列,C語言的陣列變數是指向陣列第一個元素的指標;而Go語言的陣列是一個值,Go語言中的陣列是值型別,一個陣列變數就表示著整個陣列,意味著Go語言的陣列在傳遞的時候,傳遞的是原陣列的拷貝。你可以理解為Go語言的陣列是一種有序的struct

slice

切片是一個很小的物件,是對陣列進行了抽象,並提供相關的操作方法。切片有三個屬性欄位:長度、容量和指向陣列的指標。

image

上圖中,ptr指的是指向array的pointer,len是指切片的長度, cap指的是切片的容量。現在,我想你對陣列和切片有了一個本質的認識。

切片有多種宣告方式,每種初始化方式對應的邏輯圖是怎樣的呢?

對於s := make([]byte, 5)s := []byte{...}的方式

image

對於s = s[2:4]的方式

image

對於nil的切片即var s []byte對應的邏輯圖是

image

在此有一個說明:nil切片和切片是不太一樣的,空切片即s := make([]byte, 0)或者s := []byte{}出來的切片 空切片的邏輯圖為:

image

空切片指標不為nil,而nil切片指標為nil。但是,不管是空切片還是nil切片,對其呼叫內建函式append()lencap的效果都是一樣的,感受不到任何區別。

擴容

slice這種資料結構便於使用和管理資料集合,可以理解為是一種“動態陣列”,slice也是圍繞動態陣列的概念來構建的。既然是動態陣列,那麼slice是如何擴容的呢?

請記住以下兩條規則:

  • 如果切片的容量小於1024個元素,那麼擴容的時候slice的cap就翻番,乘以2;一旦元素個數超過1024個元素,增長因子就變成1.25,即每次增加原來容量的四分之一。
  • 如果擴容之後,還沒有觸及原陣列的容量,那麼,切片中的指標指向的位置,就還是原陣列,如果擴容之後,超過了原陣列的容量,那麼,Go就會開闢一塊新的記憶體,把原來的值拷貝過來,這種情況絲毫不會影響到原陣列。

知道了一下規則,請看下面程式,試問輸出結果:

import (
    "fmt"
)
func main(){
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(append(append(slice, 50), 100), 150)
    newSlice[1] += 1
    fmt.Println(slice)
}
複製程式碼

輸出:

[10 20]
複製程式碼

答對了嗎?

參考文獻: 《Go in action》

https://blog.golang.org/go-slices-usage-and-internals

更多精彩內容,請關注我的微信公眾號 網際網路技術窩 或者加微信共同探討交流:

快速理解Go陣列和切片的內部實現原理

相關文章