Go 切片只需這一篇!

前言

大家好,我是盼盼!

切片在 golang 是一種很重要的數據結構,大家平時工作和麪試都會遇到,而且切片需要注意的點比較多,只有深入去理解它,才能避免踩坑。下面開始發車。

數組

數組是內置類型,是一組同類型數據的集合,它是值類型,通過從 0 開始的下標索引訪問元素值。

在初始化後長度是固定的,無法修改其長度。當作爲方法的參數傳入時將複製一份數組而不是引用同一指針。

數組的長度也是其類型的一部分,通過內置函數 len(array) 獲取其長度。

還有幾點要注意的:

  1. Go 中的數組是值類型,如果你將一個數組賦值給另外一個數組,那麼,實際上就是將整個數組拷貝一份。

  2. 如果 Go 中的數組作爲函數的參數,那麼實際傳遞的參數是一份數組的拷貝,而不是數組的指針, 修改數組的值需要傳遞數組的指針。

  3. array 的長度也是 Type 的一部分,這樣就說明 [1]int 和 [2]int 是不一樣的。

//值傳遞,傳的是副本
func updateArr([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))
}

打印:
[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([]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% 的容量 (隨着語言的演化,這種增長算法可能會有所改變)。

總結

切片爲我們操作集合類型的數據提供了便利的方式,又能夠高效的在函數間進行傳遞,因此在代碼中切片類型被使用的相當廣泛。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KGICSmld2-sWPOif_9AofA