【Go進階—資料結構】slice

與昊發表於2021-10-03

slice 又稱動態陣列,依託陣列實現,但比陣列更靈活,在 Go 語言中一般都會使用 slice 而非 array。

特性

宣告和初始化 slice 的方式除了從陣列中直接切取外,主要還有以下幾種:變數宣告、字面量宣告、使用內建函式 new() 和 make(),一般推薦使用 make() 函式來初始化。

實現原理

資料結構

原始碼 src/runtime/slice.go:slice 定義了 slice 的資料結構:

type slice struct {
    array unsafe.Pointer // 底層陣列指標
    len   int            // 長度
    cap   int            // 容量
}

相關操作

建立

當我們使用 make() 建立 slice 時,可以同時指定長度和容量,底層會分配一個陣列,陣列的長度即容量。

例如,slice := make([]int, 5, 10)所建立的 slice,結構如下所示:

image.png

擴容

向 slice 追加元素時,如果 slice 的容量不足以容納追加的元素時,將會觸發擴容。擴容實際上是重新分配一塊更大的記憶體,將原 slice 拷貝進新 slice,再將資料追加進去。這也是為什麼我們經常能見到類似於 s := append(s, 1) 這種寫法的原因。

擴容時容量的變化遵循以下基本規則:

  • 原 slice 容量小於 1024,則新 slice 的容量擴大為原來的 2 倍;
  • 原 slice 容量大於等於 1024,則 新 slice 的容量擴大為原來的 1.25 倍。

不過,實際過程中還會綜合考慮元素的型別及其記憶體分配策略,在該規則的基礎上做一些微調(如記憶體對齊)。

下面有一段程式碼示例,觀察函式的輸出,結合本部分內容,可以很好地明白擴容的內部機制:

package main

import "fmt"

func SliceRise(s []int) {
    s = append(s, 0)
    for i := range s {
        s[i]++
    }
}

func main() {
    s1 := []int{1, 2}
    s2 := s1
    s2 = append(s2, 3)
    SliceRise(s1)
    SliceRise(s2)
    fmt.Println(s1, s2)
}
拷貝

拷貝兩個 slice 時,會將源 slice 的元素逐個拷貝到目的 slice 指向的底層陣列中,拷貝數量取決於兩個 slice 長度的較小值。例如長度為 10 的 slice 拷貝到長度為 5 的 slice 中時,只會拷貝 5 個元素,過程中並不會觸發擴容。

一些Tips

我們常說 slice 是所謂的引用型別,主要就是因為它的結構內部含有底層陣列的指標,因此在函式內部修改 slice 中的元素的話,外部變數也會受到影響。

我們可以使用 a[low:high] 表示式切取字串,此時會產生新的字串,用這個方法可以快速擷取字串。

如果我們是從字串或陣列上切取 slice 的話,表示式 a[low:high] 需滿足: 0 <= low <= high <= len(a),如果不滿足則會觸發 panic。但是如果切取物件是另一個 slice 的話,low 和 high 的最大取值就不是 a 的長度,而是 a 的容量。在實際使用中,我們需要注意這一點。

相關文章