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並不會影響原來的切片(底層並不是同一個陣列了)。
以上都是切片中的一些細節性問題,如有問題還請各位指正