Go 切片只需這一篇!
本文轉(zhuǎn)載自微信公眾號(hào)「盼盼編程」,作者盼盼編程。轉(zhuǎn)載本文請(qǐng)聯(lián)系盼盼編程公眾號(hào)。
前言
大家好,我是盼盼!
切片在 golang 是一種很重要的數(shù)據(jù)結(jié)構(gòu),大家平時(shí)工作和面試都會(huì)遇到,而且切片需要注意的點(diǎn)比較多,只有深入去理解它,才能避免采坑。下面開始發(fā)車。
數(shù)組
數(shù)組是內(nèi)置類型,是一組同類型數(shù)據(jù)的集合,它是值類型,通過(guò)從0開始的下標(biāo)索引訪問(wèn)元素值。
在初始化后長(zhǎng)度是固定的,無(wú)法修改其長(zhǎng)度。當(dāng)作為方法的參數(shù)傳入時(shí)將復(fù)制一份數(shù)組而不是引用同一指針。
數(shù)組的長(zhǎng)度也是其類型的一部分,通過(guò)內(nèi)置函數(shù)len(array)獲取其長(zhǎng)度。
還有幾點(diǎn)要注意的:
- Go中的數(shù)組是值類型,如果你將一個(gè)數(shù)組賦值給另外一個(gè)數(shù)組,那么,實(shí)際上就是將整個(gè)數(shù)組拷貝一份。
- 如果Go中的數(shù)組作為函數(shù)的參數(shù),那么實(shí)際傳遞的參數(shù)是一份數(shù)組的拷貝,而不是數(shù)組的指針,修改數(shù)組的值需要傳遞數(shù)組的指針。
- array的長(zhǎng)度也是Type的一部分,這樣就說(shuō)明[1]int和[2]int是不一樣的。
- //值傳遞,傳的是副本
- func updateArr(b [3]int) {
- b[0] = 3
- }
- //傳指針,[3]int是一個(gè)類型
- func updateArrPoint(b *[3]int) {
- b[0] = 3
- }
- func main() {
- //常見(jiàn)兩種初始化方式
- //var b = [...]int{1, 2, 3}
- var b = [3]int{1, 2, 3}
- updateArr(b)
- fmt.Println(b)
- updateArrPoint(&b)
- fmt.Println(b)
- //計(jì)算數(shù)組長(zhǎng)度和容量
- fmt.Println(len(b))
- fmt.Println(cap(b))
- }
- 打?。?nbsp;
- [1 2 3]
- [3 2 3]
- 3
- 3
切片
Go中提供了一種靈活,功能強(qiáng)悍的內(nèi)置類型Slices切片(“動(dòng)態(tài)數(shù)組"),與數(shù)組相比切片的長(zhǎng)度是不固定的,可以追加元素,在追加時(shí)可能使切片的容量增大。
切片中有兩個(gè)概念:一是len長(zhǎng)度,二是cap容量,長(zhǎng)度是指已經(jīng)被賦過(guò)值的最大下標(biāo)+1,可通過(guò)內(nèi)置函數(shù)len()獲得。
容量是指切片目前可容納的最多元素個(gè)數(shù),可通過(guò)內(nèi)置函數(shù)cap()獲得。切片是引用類型,因此在當(dāng)傳遞切片時(shí)將引用同一指針,修改值將會(huì)影響其他的對(duì)象。
- s := []int {1,2,3 } //直接初始化切片
- s := arr[:] //用數(shù)組初始化切片
- s = make([]int, 3) //make初始化,有3個(gè)元素的切片, len和cap都為3
- s = make([]int, 2, 3) //make初始化,有2個(gè)元素的切片, len為2, cap為3
- a = append(a, 1) // 追加1個(gè)元素
- a = append(a, 1, 2, 3) // 追加多個(gè)元素, 手寫解包方式
- a = append(a, []int{1,2,3}...) // 追加一個(gè)切片, 切片需要解包
不過(guò)要注意的是,在容量不足的情況下,append的操作會(huì)導(dǎo)致重新分配內(nèi)存,可能導(dǎo)致巨大的內(nèi)存分配和復(fù)制數(shù)據(jù)代價(jià)。
a = append([]int{0}, a...) 切片頭部添加元素。在開頭一般都會(huì)導(dǎo)致內(nèi)存的重新分配,而且會(huì)導(dǎo)致已有的元素全部復(fù)制1次。
因此,從切片的開頭添加元素的性能一般要比從尾部追加元素的性能差很多。
- //切片是地址傳遞
- func updateSlice(a []int) {
- a[0] = 3
- }
- func main() {
- //切片
- var a = []int{1, 2, 3}
- c := make([]int, 5)
- copy(c, a)
- updateSlice(c)
- fmt.Println(c)
- }
- 打印
- [3 2 3 0 0]
切片的內(nèi)部實(shí)現(xiàn)
切片是一個(gè)很小的對(duì)象,它對(duì)底層的數(shù)組(內(nèi)部是通過(guò)數(shù)組保存數(shù)據(jù)的)進(jìn)行了抽象,并提供相關(guān)的操作方法。
切片是一個(gè)有三個(gè)字段的數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)包含 Golang 需要操作底層數(shù)組的元數(shù)據(jù):
這 3 個(gè)字段分別是指向底層數(shù)組的指針、切片訪問(wèn)的元素的個(gè)數(shù)(即長(zhǎng)度)和切片允許增長(zhǎng)到的元素個(gè)數(shù)(即容量)。
nil 和空切片
有時(shí),程序可能需要聲明一個(gè)值為 nil 的切片(也稱nil切片)。只要在聲明時(shí)不做任何初始化,就會(huì)創(chuàng)建一個(gè) nil 切片。
- var num []int
在 Golang 中,nil 切片是很常見(jiàn)的創(chuàng)建切片的方法。nil 切片可以用于很多標(biāo)準(zhǔn)庫(kù)和內(nèi)置函數(shù)。在需要描述一個(gè)不存在的切片時(shí),nil 切片會(huì)很好用。比如,函數(shù)要求返回一個(gè)切片但是發(fā)生異常的時(shí)候。下圖描述了 nil 切片的狀態(tài):
空切片和 nil 切片稍有不同,下面的代碼分別通過(guò) make() 函數(shù)和字面量的方式創(chuàng)建空切片:
- num := make([]int, 0) // 使用 make 創(chuàng)建空的整型切片
- num := []int{} // 使用切片字面量創(chuàng)建空的整型切片
空切片的底層數(shù)組中包含 0 個(gè)元素,也沒(méi)有分配任何存儲(chǔ)空間。想表示空集合時(shí)空切片很有用,比如,數(shù)據(jù)庫(kù)查詢返回 0 個(gè)查詢結(jié)果時(shí)。
不管是使用 nil 切片還是空切片,對(duì)其調(diào)用內(nèi)置函數(shù) append()、len() 和 cap() 的效果都是一樣的。
通過(guò)切片創(chuàng)建新的切片
切片之所以被稱為切片,是因?yàn)閯?chuàng)建一個(gè)新的切片,也就是把底層數(shù)組切出一部分。通過(guò)切片創(chuàng)建新切片的語(yǔ)法如下:
- slice[i:j]
- slice[i:j:k]
其中 i 表示從 slice 的第幾個(gè)元素開始切,j 控制切片的長(zhǎng)度(j-i),k 控制切片的容量(k-i),如果沒(méi)有給定 k,則表示切到底層數(shù)組的最尾部。下面是幾種常見(jiàn)的簡(jiǎn)寫形式:
- slice[i:] // 從 i 切到最尾部
- slice[:j] // 從最開頭切到 j(不包含 j)
- slice[:] // 從頭切到尾,等價(jià)于復(fù)制整個(gè) slice
讓我們通過(guò)下面的例子來(lái)理解通過(guò)切片創(chuàng)建新的切片的本質(zhì):
- // 創(chuàng)建一個(gè)整型切片
- // 其長(zhǎng)度和容量都是 5 個(gè)元素
- num := []int{1, 2, 3, 4, 5}
- // 創(chuàng)建一個(gè)新切片
- // 其長(zhǎng)度為 2 個(gè)元素,容量為 4 個(gè)元素
- myNum := slice[1:3]
執(zhí)行上面的代碼后,我們有了兩個(gè)切片,它們共享同一段底層數(shù)組,但通過(guò)不同的切片會(huì)看到底層數(shù)組的不同部分:
注意:截取新切片時(shí)的原則是 "左含右不含"。所以 myNum 是從 num 的 index=1 處開始截取,截取到 index=3 的前一個(gè)元素,也就是不包index=3 這個(gè)元素。
所以,新的 myNum 是由 num 中的第2個(gè)元素、第3個(gè)元素組成的新的切片構(gòu),長(zhǎng)度為 2,容量為 4。切片 num 能夠看到底層數(shù)組全部 5 個(gè)元素的容量,而 myNum 能看到的底層數(shù)組的容量只有 4 個(gè)元素。num 無(wú)法訪問(wèn)到底層數(shù)組的第一個(gè)元素。所以,對(duì) myNum 來(lái)說(shuō),那個(gè)元素就是不存在的。
共享底層數(shù)組的切片
需要注意的是:現(xiàn)在兩個(gè)切片 num 和 myNum 共享同一個(gè)底層數(shù)組。如果一個(gè)切片修改了該底層數(shù)組的共享部分,另一個(gè)切片也能感知到:
- // 修改 myNum 索引為 1 的元素
- // 同時(shí)也修改了原切片 num 的索引為 2 的元素
- myNum[1] = 35
把 35 賦值給 myNum 索引為 1 的元素的同時(shí)也是在修改 num 索引為 2 的元素:
切片只能訪問(wèn)到其長(zhǎng)度內(nèi)的元素
切片只能訪問(wèn)到其長(zhǎng)度內(nèi)的元素,試圖訪問(wèn)超出其長(zhǎng)度的元素將會(huì)導(dǎo)致語(yǔ)言運(yùn)行時(shí)異常。在使用這部分元素前,必須將其合并到切片的長(zhǎng)度里。下面的代碼試圖為 num 中的元素賦值:
- // 修改 newNum 索引為 3 的元素
- // 這個(gè)元素對(duì)于 newNum 來(lái)說(shuō)并不存在
- newNum[3] = 45
上面的代碼可以通過(guò)編譯,但是會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤:panic: runtime error: index out of range
切片擴(kuò)容
相對(duì)于數(shù)組而言,使用切片的一個(gè)好處是:可以按需增加切片的容量。
Golang 內(nèi)置的 append() 函數(shù)會(huì)處理增加長(zhǎng)度時(shí)的所有操作細(xì)節(jié)。要使用 append() 函數(shù),需要一個(gè)被操作的切片和一個(gè)要追加的值,當(dāng) append() 函數(shù)返回時(shí),會(huì)返回一個(gè)包含修改結(jié)果的新切片。
函數(shù) append() 總是會(huì)增加新切片的長(zhǎng)度,而容量有可能會(huì)改變,也可能不會(huì)改變,這取決于被操作的切片的可用容量。
- num := []int{1, 2, 3, 4, 5}
- // 創(chuàng)建新的切片,其長(zhǎng)度為 2 個(gè)元素,容量為 4 個(gè)元素
- myNum := num[1:3]
- // 使用原有的容量來(lái)分配一個(gè)新元素
- // 將新元素賦值為 60
- myNum = append(myNum, 60)
執(zhí)行上面的代碼后的底層數(shù)據(jù)結(jié)構(gòu)如下圖所示:
此時(shí)因?yàn)?myNum 在底層數(shù)組里還有額外的容量可用,append() 函數(shù)將可用的元素合并入切片的長(zhǎng)度,并對(duì)其進(jìn)行賦值。
由于和原始的切片共享同一個(gè)底層數(shù)組,myNum 中索引為 3 的元素的值也被改動(dòng)了。
如果切片的底層數(shù)組沒(méi)有足夠的可用容量,append() 函數(shù)會(huì)創(chuàng)建一個(gè)新的底層數(shù)組,將被引用的現(xiàn)有的值復(fù)制到新數(shù)組里,再追加新的值,此時(shí) append 操作同時(shí)增加切片的長(zhǎng)度和容量:
- // 創(chuàng)建一個(gè)長(zhǎng)度和容量都是 4 的整型切片
- num := []int{1, 2, 3, 4}
- // 向切片追加一個(gè)新元素
- // 將新元素賦值為 5
- myNum := append(num, 5)
當(dāng)這個(gè) append 操作完成后,newSlice 擁有一個(gè)全新的底層數(shù)組,這個(gè)數(shù)組的容量是原來(lái)的兩倍:
函數(shù) append() 會(huì)智能地處理底層數(shù)組的容量增長(zhǎng)。
在切片的容量小于 1000 個(gè)元素時(shí),總是會(huì)成倍地增加容量。一旦元素個(gè)數(shù)超過(guò) 1000,容量的增長(zhǎng)因子會(huì)設(shè)為 1.25,也就是會(huì)每次增加 25%的容量(隨著語(yǔ)言的演化,這種增長(zhǎng)算法可能會(huì)有所改變)。
總結(jié)
切片為我們操作集合類型的數(shù)據(jù)提供了便利的方式,又能夠高效的在函數(shù)間進(jìn)行傳遞,因此在代碼中切片類型被使用的相當(dāng)廣泛。