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,結構如下所示:
擴容
向 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 的容量。在實際使用中,我們需要注意這一點。