快速理解Go數組和切片的內部實現原理

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

數組

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

這裏寫圖片描述

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

slice

切片是一個很小的對象,是對數組進行了抽象,並提供相關的操作方法。切片有三個屬性字段:長度、容量和指向數組的指針。

這裏寫圖片描述

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

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

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

空切片指針不爲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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章