雖然說 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,因為新的切片容量比舊的大,會開辟新的地址,拷貝數據,降低性能。