1. 程式人生 > >Go語言中關於切片容量與其底層指標的思考

Go語言中關於切片容量與其底層指標的思考

Go語言中的切片是常用的一種資料型別,其中切片的底層是陣列,切片常用的屬性有長度和容量。

其中長度很容易理解,但是容量相對複雜一些。

切片提供了計算容量的函式 cap() 可以測量切片最長可以達到多少:它等於切片的長度 + 陣列除切片之外的長度。

以下有幾個例項,第一:

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
testSlice := slice[1:2]
fmt.Println("cap slice:", cap(slice))
fmt.Println("cap newSlice:", cap(newSlice))
fmt.Println("cap test:", cap(testSlice))

分別列印什麼呢?

答案是:

cap slice: 5
cap newSlice: 4
cap test: 4

首先,我們看slice的宣告方法,直接聲明瞭一個元素為int的切片,並且賦值,可以理解為底層是一個5個元素的陣列,所以slice的容量為5,那麼從slice上擷取得到的newSlice和testSlice長度不同,為何容量是一樣的呢?我們看下邊這張圖:

其中切片y是x從下標1開始,長度為2,容量為4,指標指向的位置為切片的起始位置,所以不難理解,為什麼前一個例子總newSlice與testSlice的容量為何相同了,是因為底層陣列的長度是一樣的。可以簡單記憶為:切片的容量是隻與切片的起始下標有關,是底層陣列減去起始位置。

再看第二個例子:

我們常見的切片中只包含一個冒號,用來標識起始位置與結束位置(左閉右開),起始從Go1.2起,支援第三個引數,用來指定該切片的容量,如上圖所示,有切片slice[i:j:k],其中切片的長度為j-i,容量為k-i。

接下來是第三個例項:

package main

import "fmt"

func main() {
    var a []int
    printSlice("a", a)

    // append works on nil slices.
    a = append(a, 0)
    printSlice("a", a)

    // the slice grows as needed.
    a = append(a, 1)
    printSlice("a", a)

    // we can add more than one element at a time.
    a = append(a, 2, 3, 4)
    printSlice("a", a)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
    s, len(x), cap(x), x)
}  

會輸出什麼呢?

答案是:

a len=0 cap=0 []
a len=1 cap=1 [0]
a len=2 cap=2 [0 1]
a len=5 cap=6 [0 1 2 3 4]      

這裡邊包含了切片長度擴充套件的原則:

當切片容量難以滿足切片長度時,需要進行擴容,由於增加切片容量需要消耗效能,所以Go預設的是會將切片的容量提高一倍,類似於網路中視窗大小每次也是乘以二。

那麼上圖中為什麼第三次append執行了之後,容量變成了6呢?我認為是由於同時添加了多個元素,所以會根據之前的步長也就是2反覆的擴容,直到容量夠用,為了驗證這個猜想,我們將第三次append改為2,3,4,5,6,7,8,得到的結果是

a len=9 cap=10 [0 1 2 3 4 5 6 7 8]

但是如果我們把多個元素拆開進行append就會驗證上邊的預設規則,程式碼如下:

package main

import "fmt"

func main() {
	var a []int
	printSlice("a", a)

	// append works on nil slices.
	a = append(a, 0)
	printSlice("a", a)

	// the slice grows as needed.
	a = append(a, 1)
	printSlice("a", a)

	a = append(a, 2)
	printSlice("a", a)

	a = append(a, 3)
	printSlice("a", a)

	a = append(a, 4)
	printSlice("a", a)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
	s, len(x), cap(x), x)
}

列印結果如下:

a len=0 cap=0 []
a len=1 cap=1 [0]
a len=2 cap=2 [0 1]
a len=3 cap=4 [0 1 2]
a len=4 cap=4 [0 1 2 3]
a len=5 cap=8 [0 1 2 3 4]

可以看到,切片的容量是0->1->2->4->8,當切片容量不足時,繼續新增元素容量會擴大一倍。

PS: 在Stack Overflow連結中,這裡其實是有異議的,在不同的架構/Go版本中,擴容的策略可能會有不同,詳情請參考連結中的回覆。這裡不做過多說明,有不同意見歡迎在評論中討論。

最後一個例項,其實與之前一篇關於切片是否是傳引用呼叫的博文有關,有興趣的同學可以翻看一下

先看程式碼:

package main

import (
	"fmt"
)

func main() {
	slice := []int{10, 20, 30, 40, 50}
	newSlice := slice[1:3]
	// 使用原有切片劃分出新切片,容量為4
	// 將新元素賦值為 60,會改變底層陣列中的元素
	newSlice = append(newSlice, 60) //這裡會影響原切片的值,40->60
	fmt.Println(newSlice)
	fmt.Println(slice)
	
	slice1 := []int{10, 20, 30, 40, 50}
	newSlice1 := slice1[1:3:3] // 新切片容量為2
	newSlice1 = append(newSlice1, 60) //這裡就不會影響
	fmt.Println(newSlice1)
	fmt.Println(slice1)
}

列印的結果如下:

[20 30 60]
[10 20 30 60 50]
[20 30 60]
[10 20 30 40 50]

為什麼第一次會影響原切片,而第二次不會呢?

其實是由於第一次append元素並沒有發生擴容,所以其實會修改下標為3的元素,即將40改為了60;但第二次在slice1中,由於切片的容量指定為2,所以再次append的時候發生了擴容,Go會將原來的值拷貝一份並指向新的底層陣列,所以append並不會影響原來的切片(底層並不是同一個陣列了)。

以上都是切片中的一些細節性問題,如有問題還請各位指正