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