咱們來重新認識一下Golang的切片
今天廢話不用多說,咱們來直接進入正題
切片究竟是什么?
在聊切片之前,我們先來看一下golang中的數組,大家都知道golang其實是c語言寫的,那么在數組這一塊golang和c語言的含義一樣么?當然是不一樣的。
golang數組
- Go數組是值語義的,這意味著一個數組變量表示的是「整個數組」。
- Go語言中傳遞數組是純粹的「值拷貝」。
c語言數組
- 數組變量可視為指向數組「第一個元素的指針」。
因為golang中數組是純粹的值拷貝,所以在golang中,更地道的方式是使用「切片」, 「切片之于數組就像是文件描述符之于文件」數組更多是“退居幕后”,承擔的是底層存儲空間的角色;而切片則走向“前臺”,為底層的存儲(數組)打開了一個訪問的“窗口”。
切片和數組的關系
其實通過golang源碼也可以看出來,其實切片就是數組的指針。
//$GOROOT/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
如何聲明一個切片?
方式一
s := make([]byte, 5)
我們看到通過上述語句創建的切片,編譯器會自動為切片建立一個「底層數組」,如果沒有在make中指定cap參數,那么cap = len,即編譯器建立的數組長度為len。
方式二(數組切片化)
u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s := u[3:7]
數組切片化
- 切片s打開了一個操作數組u的窗口。
- 切片截取數組是「左包含右不包含」的原則。比如u[3,7]為包含u[3]但是不包含u[7]。
- 「切片的長度len」為4,計算方式為(high-low),在這個case中也就是7-3=4。
- 「切片的容量cap」為s的第一個元素s[0]到數組u的末尾,所以是7。
當然可以基于一個數組建立多個切片
u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s1 := u[1:5]
s2 := u[6:9]
s3 := u[3:7]
基于一個數組建立多個切片
也可以基于已有切片再次創建切片,也叫reslicing
u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s1 := u[1:5]
s2 := s1[2:4]
reslicing
動態擴容
在講動態擴容之前,我們先來看一些例子。
// chapter3/sources/slice_append.go
var s []int // s被賦予零值nil
s = append(s, 11)
fmt.Println(len(s), cap(s)) //1 1
s = append(s, 12)
fmt.Println(len(s), cap(s)) //2 2
s = append(s, 13)
fmt.Println(len(s), cap(s)) //3 4
s = append(s, 14)
fmt.Println(len(s), cap(s)) //4 4
s = append(s, 15)
fmt.Println(len(s), cap(s)) //5 8
我們看到切片s的len值是線性增長的,但cap值卻呈現出不規則的變化。通過下圖我們更容易看清楚多次append操作究竟是如何讓切片進行動態擴容的。
動態擴容
我們看到append會根據切片的需要,在「當前底層數組容量無法滿足」的情況下,「動態分配新的數組」,新數組長度會按一定算法擴展(參見$GOROOT/src/runtime/slice.go中的growslice函數)。新數組建立后,append會把「舊數組中的數據復制到新數組中」,之后新數組便成為切片的底層數組,舊數組后續會被「垃圾回收」掉。
這樣的append操作有時會給Gopher帶來一些困惑,比如通過語法u[low: high]形式進行數組切片化而創建的切片,一旦切片cap觸碰到數組的上界,再對切片進行append操作,切片就會和原數組解除綁定。
小結練習
根據自己對切片的理解,先看看自己能不能想到每一步結果都會輸出啥。
// chapter3/sources/slice_unbind_orig_array.go
func main() {
u := []int{11, 12, 13, 14, 15}
fmt.Println("array:", u) // [11, 12, 13, 14, 15]
s := u[1:3]
fmt.Printf("slice(len=%d, cap=%d): %v\n", len(s), cap(s), s) // [12, 13]
s = append(s, 24)
fmt.Println("after append 24, array:", u)
fmt.Printf("after append 24, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
s = append(s, 25)
fmt.Println("after append 25, array:", u)
fmt.Printf("after append 25, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
s = append(s, 26)
fmt.Println("after append 26, array:", u)
fmt.Printf("after append 26, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
s[0] = 22
fmt.Println("after reassign 1st elem of slice, array:", u)
fmt.Printf("after reassign 1st elem of slice, slice(len=%d, cap=%d): %v\n", len(s), cap(s), s)
}
答案揭曉
$go run slice_unbind_orig_array.go
array: [11 12 13 14 15]
slice(len=2, cap=4): [12 13]
after append 24, array: [11 12 13 24 15]
after append 24, slice(len=3, cap=4): [12 13 24]
after append 25, array: [11 12 13 24 25]
after append 25, slice(len=4, cap=4): [12 13 24 25]
after append 26, array: [11 12 13 24 25]
after append 26, slice(len=5, cap=8): [12 13 24 25 26]
after reassign 1st elem of slice, array: [11 12 13 24 25]
after reassign 1st elem of slice, slice(len=5, cap=8): [22 13 24 25 26]
我們看到在添加元素25之后,切片的元素已經觸碰到底層數組u的邊界;此后再添加元素26,append發現底層數組已經無法滿足添加新元素的要求,于是新創建了一個底層數組(數組長度為cap(s)的2倍,即8),并將原切片的元素復制到新數組中。在這之后,即便再修改切片中的元素值,原數組u的元素也沒有發生任何改變,因為此時切片s與數組u已經解除了綁定關系,s已經不再是數組u的描述符了。