雖說 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)
來獲取。app
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
Go 提供了內建的 append 函數,爲切片追加新的元素。函數
func append(s []T, vs ...T) []Tappend 的結果是一個包含原切片全部元素加上新添加元素的切片。oop
下面分兩種狀況描述了向切片追加新元素後切片長度和容量的變化。
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。ui
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) }
執行結果以下:code
[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]
而這個切片在追加元素後,其容量和指針地址發生了變化,但底層數組未變。
當切片的底層數組不足以容納全部給定值時,它就會分配一個更大的數組。返回的切片會指向這個新分配的數組。
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,每增長一個元素,其容量翻番。
Example2 中,由於切片的底層數組沒有足夠的可用容量,append() 函數會建立一個新的底層數組,將被引用的現有的值複製到新數組裏,再追加新的值,因此原數組沒有變化,不是我想象中的[1 2 3 4 5],
擴容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)
執行上面代碼後的底層數據結構以下圖所示: