
Go 語(yǔ)言中雖然有數(shù)組,但在代碼中直接用的比較少,而是會(huì)間接的用到,slice 存儲(chǔ)數(shù)據(jù)就是用的 數(shù)組,甚至可以認(rèn)為數(shù)組是為了 slice 存在。Go 語(yǔ)言中的 slice 可以當(dāng)做數(shù)組來(lái)使用,也可以當(dāng)做其他語(yǔ)言中的 List 來(lái)使用。
slice 表示一個(gè)擁有相同類型元素的可變長(zhǎng)的序列。可變長(zhǎng)是 slice 最重要的一個(gè)特性,這是它和數(shù)組最不同的地方,既然 slice 是依靠數(shù)組而存在的,那么 slice 是如何做到可變長(zhǎng)的呢,這篇文章就來(lái)聊一下。
先從 slice 的創(chuàng)建說(shuō)起。
1. slice 的創(chuàng)建
slice 可以主動(dòng)創(chuàng)建,也可從現(xiàn)有的數(shù)組中獲取。
帶初始化元素的方式:
nums := []int{0, 1, 2, 3, 4, 5}
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 6
不帶初始化元素的方式,下面的這種方式會(huì)自動(dòng)填充零值:
nums := make([]int, 6)
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 6
還可以創(chuàng)建一個(gè)更大容量的 slice:
nums := make([]int, 6, 12)
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 12
在這里需要需要注意,雖然 slice 容量有 12,但是卻不能直接訪問(wèn) slice 長(zhǎng)度范圍外的元素,否則會(huì)出現(xiàn) panic:
fmt.Println(nums[6]) // panic
要么通過(guò)擴(kuò)展 slice 的長(zhǎng)度,要么使用 append 函數(shù)來(lái)添加元素。
nums = nums[:len(nums)+1] // 將 slice 的長(zhǎng)度擴(kuò)展大 1
nums = append(nums, 1) // 使用 append 將 nums 的長(zhǎng)度加 1
在使用上面的方式創(chuàng)建 slice 時(shí),Go 會(huì)自動(dòng)的創(chuàng)建一個(gè)底層數(shù)組來(lái)存儲(chǔ)數(shù)據(jù),slice 本身并不會(huì)存儲(chǔ)數(shù)據(jù)。
還有一種方式是通過(guò)數(shù)組來(lái)創(chuàng)建 slice:
numsArr := [...]int{0, 1, 2, 3, 4, 5}
numsSlice := numsArr[:] // 這表示 slice 容納數(shù)組的所有元素
fmt.Println(len(numsSlice)) // 6
fmt.Println(cap(numsSlice)) // 6
2. slice 的結(jié)構(gòu)
一般 可以把 slice 看成是一個(gè)復(fù)合結(jié)構(gòu),結(jié)構(gòu)如下,由三部分組成:指針、長(zhǎng)度和容量:
type Slice struct {
ptr *T
len, cap int
}
slice 本身不會(huì)存儲(chǔ)任何數(shù)據(jù),slice 通過(guò)指針指向底層數(shù)組的某一個(gè)位置,這個(gè)位置就是 slice 的初始位置。長(zhǎng)度表示當(dāng)前 slice 中的元素個(gè)數(shù),容量表示從指針的位置到底層數(shù)組的最后一個(gè)位置。
slice 可變長(zhǎng)的第一個(gè)原因就是可以通過(guò)改變指針的位置來(lái)改變 slice 的長(zhǎng)度。比如下面這樣:
numsSlice2 := numsSlice[1:3]
fmt.Println(len(numsSlice2)) // 2
fmt.Prinltn(cap(numsSlice2)) // 5
這個(gè)操作在 numsSlice 的基礎(chǔ)上創(chuàng)建的一個(gè)新的 slice,其實(shí)就是移動(dòng)了一下 slice 的指針。
另外 slice 不能使用 == 進(jìn)行比較,因?yàn)?slice 的元素是非直接的,也就是 slice 本身不存儲(chǔ)值,slice 甚至可以包含自身。另外因?yàn)?slice 的元素是靠底層的數(shù)組存儲(chǔ),所以當(dāng)?shù)讓訑?shù)組變動(dòng)的時(shí)候,slice 讀取到的值也會(huì)產(chǎn)生變化。如果 使用 == 來(lái)進(jìn)行比較,可能會(huì)產(chǎn)生很多預(yù)料之外的結(jié)果。
3. 理解 slice 的 cap
對(duì)于初學(xué)者來(lái)說(shuō),會(huì)把 slice 的長(zhǎng)度和容量搞混。
先來(lái)看下面這段代碼:
numsArr := [...]int{1, 2, 3, 4, 5, 6}
s1 := numsArr[0 : 3]
fmt.Println(s1) // [1,2,3]
fmt.Println(cap(s1)) // 6
fmt.Println(len(s1)) // 3
s2 := numsArr[3:5]
fmt.Println(s2) // [4,5]
fmt.Println(cap(s2)) // 3
fmt.Println(len(s2)) // 2
我們可以發(fā)現(xiàn),同樣是從數(shù)組上來(lái)生成 slice,但是生成之后的 slice 的容量卻不一樣,在上面而我們說(shuō)到了 slice 的初始位置靠指針來(lái)決定,s1 指針的位置在數(shù)組的最開始的位置。按照容量的定義,從指針到底層數(shù)組結(jié)束的地方,所以它的 容量還是 6。
而 s2 中,指針的位置是數(shù)組的第四個(gè)元素,長(zhǎng)度是 3,容量也是 3。這是初學(xué)者最容易犯錯(cuò)的地方。
長(zhǎng)度表示 slice 中目前可訪問(wèn)的元素個(gè)數(shù),容量則表示這個(gè) slice 在不改變底層數(shù)組的情況下,最多個(gè)擴(kuò)展到的長(zhǎng)度。如果要擴(kuò)展 slice,使用下面的操作就可以:
s2 = s2[:3] // 把上面 s2 的長(zhǎng)度從 2 擴(kuò)展到 3
fmt.Println(s2[2]) // 訪問(wèn)第三個(gè)元素
這種方式是讓 slice 可變長(zhǎng)的第二種方式。
4. 理解 append
append 方法也是讓 slice 可變長(zhǎng)的一種方式。
append 函數(shù)經(jīng)常會(huì)配合 slice 一起使用,在使用 append 的時(shí)候,我們需要使用下面的語(yǔ)法:
nums = append(nums, 1)
而且這種做法是必須的,如果沒(méi)有采用這種做法,可能會(huì)產(chǎn)生意料之外的結(jié)果。看下面的代碼:
numsArr := [...]int{1, 2, 3, 4, 5, 6}
s1 := numsArr[3:5]
fmt.Println(cap(s1)) // 3
fmt.Println(len(s1)) // 2
_ = append(s1, 1)
fmt.Println(s1[2]) // panic
上面的代碼在調(diào)用 append 之后,如果沒(méi)有使用返回的 slice,而是直接使用原來(lái)的 slice,就會(huì)產(chǎn)生越界的錯(cuò)誤。
因?yàn)?slice 本身雖然是可變長(zhǎng)的,所以如果 slice 還有容量,那么每次添加元素,都需要擴(kuò)展 slice 的長(zhǎng)度,返回的也是一個(gè)新的 slice。
另外 slice 依賴的底層數(shù)組是固定長(zhǎng)度,在使用 append 時(shí),如果底層的數(shù)組不足以存儲(chǔ)新的元素之后,就需要擴(kuò)容,擴(kuò)容之后就會(huì)產(chǎn)生一個(gè)新的 slice 返回。
可以理解 append 方法修改傳入的 slice,而我們?cè)谡{(diào)用 append 函數(shù)之后,就需要使用函數(shù)返回的結(jié)果。所以上面的代碼需要修改為:
numsArr := [...]int{1, 2, 3, 4, 5, 6}
s1 := numsArr[3:5]
fmt.Println(cap(s1)) // 3
fmt.Println(len(s1)) // 2
s1 = append(s1, 1)
fmt.Println(s1[2]) //1
5. 小結(jié)
slice 是一個(gè)很有用的數(shù)據(jù)接口,既可以當(dāng)數(shù)組用,也可以當(dāng) list 使用。在使用 slice 的過(guò)程中,一定要注意 slice 本身不存儲(chǔ)數(shù)據(jù),它只是在一個(gè)底層數(shù)組上,截取不同長(zhǎng)度序列,可以說(shuō) slice 就是一個(gè)數(shù)組的子序列。
另外我們說(shuō)到了 slice 可變長(zhǎng)的幾種方式,一種是通過(guò)移動(dòng) slice 的指針,改變 slice 的長(zhǎng)度,第二種是 slice 容量范圍內(nèi)擴(kuò)展長(zhǎng)度,第三種是通過(guò) append 方式,這種方式會(huì)創(chuàng)建一個(gè)新的 slice。
另外我們經(jīng)常會(huì)對(duì) string 做子串的截取操作和這里 slice 工作原理是一樣的。
文 / Rayjun
本文首發(fā)于微信公眾號(hào)【Rayjun】