Go的切片:長度和容量
雖然說 Go 的語法在很大程度上和 PHP 很像,但 PHP 中卻是沒有“切片”這個概念的,在學習的過程中也遇到了一些困惑,遂做此筆記。
困惑1:使用 append 函式為切片追加元素後,切片的容量時變時不變,其擴容機制是什麼?
困惑2:更改切片的元素會修改其底層陣列中對應的元素。為什麼有些情況下更改了切片元素,其底層陣列元素沒有更改?
一、切片的宣告
切片可以看成是陣列的引用。在 Go 中,每個陣列的大小是固定的,不能隨意改變大小,切片可以為陣列提供動態增長和縮小的需求,但其本身並不儲存任何資料。
/* * 這是一個數組的宣告 */ var a [5]int //只指定長度,元素初始化為預設值0 var a [5]int{1,2,3,4,5} /* * 這是一個切片的宣告:即宣告一個沒有長度的陣列 */ // 陣列未建立 // 方法1:直接初始化 var s []int //宣告一個長度和容量為 0 的 nil 切片 var s []int{1,2,3,4,5} // 同時建立一個長度為5的陣列 // 方法2:用make()函式來建立切片:var 變數名 = make([]變數型別,長度,容量) var s = make([]int, 0, 5) // 陣列已建立 // 切分陣列:var 變數名 []變數型別 = arr[low, high],low和high為陣列的索引。 var arr = [5]int{1,2,3,4,5} var slice []int = arr[1:4] // [2,3,4]
二、切片的長度和容量
切片的長度是它所包含的元素個數。
切片的容量是從它的第一個元素到其底層陣列元素末尾的個數。
切片 s 的長度和容量可通過表示式 len(s)
和 cap(s)
來獲取。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10,cap=10
s1 := s[0:5] // [0 1 2 3 4] len=5,cap=10
s2 := s[5:] // [5 6 7 8 9] len=5,cap=5
三、切片追加元素後長度和容量的變化
1.append 函式
Go 提供了內建的 append 函式,為切片追加新的元素。
func append(s []T, vs ...T) []T
append 的結果是一個包含原切片所有元素加上新新增元素的切片。
下面分兩種情況描述了向切片追加新元素後切片長度和容量的變化。
Example 1:
package main import "fmt" func main() { arr := [5]int{1,2,3,4,5} // [1 2 3 4 5] fmt.Println(arr) s1 := arr[0:3] // [1 2 3] printSlice(s1) s1 = append(s1, 6) printSlice(s1) fmt.Println(arr) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s) }
執行結果如下:
[1 2 3 4 5]
len=3 cap=5 0xc000082030 [1 2 3]
len=4 cap=5 0xc000082030 [1 2 3 6]
[1 2 3 6 5]
可以看到切片在追加元素後,其容量和指標地址沒有變化,但底層陣列發生了變化,下標 3 對應的 4 變成了 6。
Example 2:
package main
import "fmt"
func main() {
arr := [5]int{1,2,3,4} // [1 2 3 4 0]
fmt.Println(arr)
s2 := arr[2:] // [3 4 0]
printSlice(s2)
s2 = append(s2, 5)
printSlice(s2)
fmt.Println(arr)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}
執行結果如下:
[1 2 3 4 0]
len=3 cap=3 0xc00001c130 [3 4 0]
len=4 cap=6 0xc00001c180 [3 4 0 5]
[1 2 3 4 0]
而這個切片在追加元素後,其容量和指標地址發生了變化,但底層陣列未變。
當切片的底層陣列不足以容納所有給定值時,它就會分配一個更大的陣列。返回的切片會指向這個新分配的陣列。
2.切片的原始碼學習
Go 中切片的資料結構可以在原始碼下的 src/runtime/slice.go
檢視。
// go 1.3.16 src/runtime/slice.go:13
type slice struct {
array unsafe.Pointer
len int
cap int
}
可以看到,切片作為陣列的引用,有三個屬性欄位:長度、容量和指向陣列的指標。
向 slice 追加元素的時候,若容量不夠,會呼叫 growslice 函式,
// go 1.3.16 src/runtime/slice.go:76
func growslice(et *_type, old slice, cap int) slice {
//...code
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// 跟據切片型別和容量計算要分配記憶體的大小
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
// ...code
}
// ...code...
// 將舊切片的資料搬到新切片開闢的地址中
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
從上面的原始碼,在對 slice 進行 append 等操作時,可能會造成 slice 的自動擴容。其擴容時的大小增長規則是:
- 如果切片的容量小於 1024,則擴容時其容量大小乘以2;一旦容量大小超過 1024,則增長因子變成 1.25,即每次增加原來容量的四分之一。
- 如果擴容之後,還沒有觸及原陣列的容量,則切片中的指標指向的還是原陣列,如果擴容後超過了原陣列的容量,則開闢一塊新的記憶體,把原來的值拷貝過來,這種情況絲毫不會影響到原陣列。
上面的兩個例子中,切片的容量均小於 1024 個元素,所以擴容的時候增長因子為 2,每增加一個元素,其容量翻番。
Example2 中,因為切片的底層陣列沒有足夠的可用容量,append() 函式會建立一個新的底層陣列,將被引用的現有的值複製到新數組裡,再追加新的值,所以原陣列沒有變化,不是我想象中的[1 2 3 4 5],
3.切片擴容的內部實現
擴容1:切片擴容後其容量不變
slice := []int{1,2,3,4,5}
// 建立新的切片,其長度為 2 個元素,容量為 4 個元素
mySlice := slice[1:3]
// 使用原有的容量來分配一個新元素,將新元素賦值為 40
mySlice = append(mySlice, 40)
執行上面程式碼後的底層資料結構如下圖所示:
擴容2:切片擴容後其容量變化
// 建立一個長度和容量都為 5 的切片
mySlice := []int{1,2,3,4,5}
// 向切片追加一個新元素,將新元素賦值為 6
mySlice = append(mySlice, 6)
執行上面程式碼後的底層資料結構如下圖所示:
四、小結
- 切片是一個結構體,儲存著切片的容量,長度以及指向陣列的指標(陣列的地址)。
- 儘量對切片設定初始容量值,以避免 append 呼叫 growslice,因為新的切片容量比舊的大,會開闢新的地址,拷貝資料,降低效能。