新聞中心
本文轉(zhuǎn)載自微信公眾號「盼盼編程」,作者盼盼編程。轉(zhuǎn)載本文請聯(lián)系盼盼編程公眾號。

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


咨詢
建站咨詢
