在前面的文章中,我和大家一起學習了一下關於 Go 語言中數組的知識,當時有提到過一個知識點:在函數中傳遞數組是非常耗資源的一件事,所以更推薦大家使用切片(slice)來這麼做。
那麼切片又是一個怎樣的東西呢?看完這篇文章你就知道了!
上一篇傳送門:Go by Example-圖解數組
初識切片切片(slice)也是一種數據結構,它和數組非常相似,但它是圍繞動態數組的概念設計的,可以按需自動改變大小。
切片的動態增長是通過內置函數 append 來實現的,這個函數可以快速且高效地增長切片。
使用切片後,不僅可以更方便地管理和使用數據集合,還可以在切片的基礎上繼續使用切片來縮小一個切片的長度範圍。
因為切片的底層就是一個數組,所以切片和數組的一些操作類似。比如:獲得切片索引、迭代切片等。
切片的對象非常小,它是一個只有 3 個欄位的數據結構,分別是:
1.指向底層數組的指針2.切片的長度3.切片的容量
了解了一下切片的好處和特性之後,我們再來看看如何創建切片吧。
創建切片創建切片的方式有兩種,一種是使用內置的 make 函數,一種是使用切片的字面量。
下面我們來看一下這兩種創建方式的使用方法。
使用 make 創建在創建之前我們先簡單地了解一下 make 函數的作用。
make 只能應用於三種數據類型:本文中的 slice、以及後面要說的 map 和 chan。
make 會為它們分配內存、初始化一個對應類型的對象並返回。注意,返回值的類型依然是被 make 的那個類型,make 只對其做了一個引用。
make 主要是用來做初始化的,記住這點,這在後面的學習中非常重要(但不是本章的後面)。
使用長度和容量聲明整型切片slice := make([]int, 4, 5)在使用 make 創建切片的時候,一共可以傳入三個參數:
1.聲明一個 int 類型的切片2.指定切片的長度3.指定切片的容量,也就是底層數組的長度
由於切片可以按需改變大小,所以在聲明類型的時候並不需要指定長度(即[123]int 這種寫法),也不需要讓編譯器自己去推斷數組的大小(即[...]int 這種寫法)。
所謂的容量其實就是切片可以增加到的最大長度。如果基於這個切片創建新的切片,新切片會和原有切片共享底層數組,也能通過後期操作來訪問多餘容量的元素。
長度和容量一樣的情況如果你在使用 make 函數創建切片的時候只使用了兩個參數的話,那麼這個切片的長度和容量將會是一樣的。
例如這裡我聲明了一個 string 類型的切片,並將它的長度和容量都設置為 6。
slice := make([]string, 6)注意:切片的容量不能小於其長度slice: = make([]int, 3, 1)這行代碼在編譯的時候是會報錯的,因為切片的容量不能小於長度,需要注意。
通過字面量創建// 創建一個字符串切片並賦值,其長度和容量都是5。slice := []string {"I", "Love", "My", "祖", "國"}
// 創建一個整型切片並賦值,其長度和容量都是3。slice := []int {10, 20, 30}另外,在使用切片字面量時,我們可以設置初始長度和容量。
// 創建一個整形切片,使用整形數字 8 初始化第 10 個元素slice := []int {9: 8}// 註:上面的這個 9 代表下標 9,也就是第 10 個元素上面的代碼表示聲明一個長度和容量都為 10 的數組,並把第 10 個元素的值設定為 8,其他位置的元素此時因為沒有賦值,所以是對應類型的零值。這裡的話因為聲明類型是 int 的關係,所以是一個 int 值的 0。
用哪種方式創建呢?這個主要得看在聲明切片的時候,是否知道裡面部分元素的值。
如果不知道的話,不管使用哪種方式都可以。
但如果想在聲明的時候順帶給元素賦值,那麼就可以選擇使用字面量的方式。
為什麼切片會和底層數組有關係呢?這個實際上和它的數據結構聲明是有關係的,切片實際上是一個結構體類型的數據結構,看一下切片類型的源碼就知道了:
type slice struct { array unsafe.Pointer // 底層數組 len int cap int}不過由於本文主要說的是切片的關係,結構體等內容就先暫且不談了,現在我們只需要知道切片底層有個數組就可以了。
圖解切片結構下面通過一張圖來對切片做一個直觀的理解,就拿下面這個例子來說:
slice := make([]int, 4, 5)slice[2] = 9把它畫成圖的話就是這個樣子的:
因為切片存的只是指向底層數組的指針而已,所以切片佔用的內寸空間是很小的。
我們來做個簡單的計算,我的電腦是 64 位的,unsafe.Pointer 類型變量佔用 8 個字節,int 類型變量佔用 8 個字節,所以算起來這個切片所佔的內存空間大小只有 8 + 8 * 2 = 24 字節,非常小。
操作切片通過切片創建切片除了上面的方式以外,我們還可以使用切片來創建切片。
// 創建一個整型切片,其長度和容量都是 5 個元素slice := []int{1, 2, 3, 4, 5}
// 創建一個新切片,其長度為 2 個元素,容量為 4 個元素newSlice := slice[1:3]執行完這段操作以後,我們就有兩個切片了,它們共享一個底層數組,我們通過一個圖來幫助理解一下這個過程。
新切片的下標 [0] 對應的實際是底層數組的下標 [1]。
如果沒有限定容量的大小,那麼可以得知:
•新切片的長度為 3-1=2。•新切片的容量為 5-1=4。(這個5代表原來切片總長度)
說到容量,還需要注意一點,它一般只是用來增加切片長度用的,我們無法通過下標去取裡面的內容。
例如:
package main
import "fmt"
func main () { slice := make([]int, 4, 5) newSlice := slice[1:3] fmt.Println(newSlice[1]) fmt.Println(newSlice[2])}運行結果會是:
panic: runtime error: index out of range使用切片創建新的切片的時候,實際上一共是有三個參數的,前面我們已經使用了兩個,現在我們來看看第三個參數。
第三個參數是用來限定新的切片的最大容量的,這個最大容量計算是從索引位置開始,加上希望容量中包含的元素的個數得到的。
舉個例子:
// 創建一個整型切片,其長度和容量都是 5 個元素slice := []int{1, 2, 3, 4, 5}
// 創建一個新切片,其長度為 3-1=2 個元素,容量為 4-1=3 個元素newSlice := slice[1:3:4]這裡我們要獲取的新切片要求是從底層數組索引 1 的位置開始,然後取其後面最多 3 個數。
執行後得到的就是我們要寫的第三個參數:4。
此時如果用圖表示的話是這個樣子:
灰色部分是新切片不能拓展到的部分,原因很簡單,因為我們把最大容量設置為了 3。
注意:如果設置的容量比可用的容量還大,就會得到一個語言運行時錯誤。
nil 切片在聲明切片時不做任何初始化,就會創建一個 nil 切片,nil 切片可以用於很多標準庫和內置函數。
// 創建 nil 整型切片var slice [] intnil 切片長度為 0,容量也為 0。
那麼這個東西有什麼用呢?比如說我們調用了一個函數,我們希望它返回一個切片,但是運行期間發生了異常,這個時候我們就可以通過判斷結果是否為 nil,得知程序是否出現異常了。
切片賦值對切片賦值就很簡單了,直接通過它的下標進行賦值即可。舉個例子:
slice := []int{10, 20, 30}slice[1] = 3得到結果如下:
注意:賦值的時候使用的索引,不能超過切片的最大索引,也就是切片的長度 - 1。
如果你需要給通過切片創建的切片進行賦值,那麼你需要注意了!前面說過新舊切片是共享同一個底層數組的,而修改切片的值實際上是在修改底層數組的值,所以這就產生了一個問題,如果對新的切片賦值,那麼舊的切片的值也會發生變化。
舉個例子:
package main
import "fmt"
func main () { slice := make ([]int, 4, 5) newSlice := slice[1:3] fmt.Printf("舊切片賦值之前:% d\n", newSlice) fmt.Printf("新切片賦值之前:% d\n", slice) newSlice[1] = 666 fmt.Printf("舊切片賦值之前:% d\n", slice) fmt.Printf("新切片賦值之前:% d\n", newSlice)}得到結果如下:
新切片賦值之前:[0 0]舊切片賦值之前:[0 0 0 0]新切片賦值之後:[0 0 666 0]舊切片賦值之後:[0 666]看到了嗎?舊切片的值也被改變了!那麼同理,如果對舊切片賦值,新切片也是會發生變化,這個就不做演示了,和上面這個結果類似。
切片增長Go 語言內置的 append 函數可以增加切片的長度,使用 append 需要一個被操作的切片和一個要追加的值。
append 必定會增加新切片的長度,而切片容量的變化則取決於被操作的切片的可用容量,可增長可不增長。
簡單來說,如果 append 操作完之後,切片內的元素個數不大於容量數,那麼新的切片就不增加容量。同樣,此時的新切片還是和之前的切片共享同一個底層數組。但是這種做法我並不建議你使用!
舉個例子:
package main
import "fmt"
func main() { slice := []int{1,2,3,4,5} newSlice := slice[1:3:4] fmt.Printf("新切片之前元素:% d\n", newSlice) fmt.Printf("舊切片之前元素:% d\n", slice) newSlice = append(newSlice, 6) fmt.Printf("舊切片增加之後元素:% d\n", slice) fmt.Printf("新切片此時元素:% d\n", newSlice)得到結果如下:
新切片賦值之前:[2 3]舊切片賦值之前:[1 2 3 4 5]舊切片增加之後元素:[1 2 3 6 5]新切片此時元素:[2 3 6]讓我們通過下面這張圖來分析一下,上面都發生了什麼。
看圖可以發現,我們在增加新切片的元素的時候,無意中修改了底層的數組元素,這導致舊的切片的值也發生了變化,所以我更推薦你使用下面這種方式對新的切片進行元素的增加:
還記得前面說的:「使用切片創建切片的時候,可以使用第三個參數限制其最大容量」麼?
這裡我們要說的就是使用 append 之後,新的切片容量增大的情況。
如果切片的底層數組沒有足夠的可用容量,append 函數會創建一個新的底層數組,然後將被引用的現有的值複製到新數組裡,再追加新的值。
也就是說,這種情況新的切片可以單獨享用底層的數組,這樣的話即使你修改了新切片的值,對舊的切片也不會造成任何影響,因為它們不再共享同一個底層數組。
舉個例子:
slice := []int{1, 2, 3, 4, 6}newSlice := append(slice, 6)當這個 append 操作完成後,newSlice 會擁有一個全新的底層數組,這個數組的容量是原來的兩倍。函數 append 會智能地處理底層數組的容量增長,在切片的容量小於 1000 個元素時,它總是會成倍地增加容量。一旦元素個數超過 1000,容量的增長倍數就會被設為 1.25,也就是說會每次增加 25% 的容量。
append 除了可以添加元素以外,還可以在一個切片中追加另一個切片。只需要通過 ... 來將第二個切片的元素做一個拆分就行了,這裡的 ... 就類似於 Python 裡的解包操作。
s1 := []int {1, 2}s2 := []int {3, 4}
fmt.Printf("% v\n", append(s1, s2...))
輸出:[1 2 3 4]切片迭代對了,切片還可以像數組那樣去迭代,只需要這樣就行了:
slice := []int {1, 2, 3, 4}for index, value := range slice { fmt.Printf("Index: % d Value: % d\n", index, value)}在函數間傳遞切片切片在函數間進行傳遞的時候,只是複製了切片的本身,不會涉及底層的數組。
前面我們計算了一下,在 64 位的機器上,切片僅佔用了 24 個字節,而在函數間傳遞 24 字節的數據是非常簡單、快速的事情。
這正是切片效率高的地方,不需要傳遞指針,也不需要處理複雜的語法,只需要複製切片,按想要的方式修改數據,然後傳遞迴一份新的切片副本就可以了,非常的簡單快捷。
參考資料•https://www.jb51.net/article/126703.htm•https://www.cnblogs.com/chenpingzhao/p/9918062.html•https://blog.csdn.net/qq_19018277/article/details/100578553•http://www.meirixz.com/archives/80658.html
文章作者:「夜幕團隊 NightTeam」 - 陳祥安
潤色、校對:「夜幕團隊 NightTeam」 - Loco
夜幕團隊成立於 2019 年,團隊包括崔慶才、周子淇、陳祥安、唐軼飛、馮威、蔡晉、戴煌金、張冶青和韋世東。
涉獵的程式語言包括但不限於 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發、對象存儲等。團隊非正亦非邪,只做認為對的事情,請大家小心。