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