《Go語言實戰筆記》(五)| Go 切片

《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org,第一時間看後續筆記。

切片也是一種數據結構,它和數組非常相似,因為他是圍繞動態數組的概念設計的,可以按需自動改變大小,使用這種結構,可以更方便的管理和使用數據集合。

內部實現

切片是基於數組實現的,它的底層是數組,它自己本身非常小,可以理解為對底層數組的抽象。因為機遇數組實現,所以它的底層的內存是連續非配的,效率非常高,還可以通過索引獲得數據,可以迭代以及垃圾回收優化的好處。

切片對象非常小,是因為它是只有3個欄位的數據結構:一個是指向底層數組的指針,一個是切片的長度,一個是切片的容量。這3個欄位,就是Go語言操作底層數組的元數據,有了它們,我們就可以任意的操作切片了。

聲明和初始化

切片創建的方式有好幾種,我們先看下最簡潔的make方式。

slice:=make([]int,5)n

使用內置的make函數時,需要傳入一個參數,指定切片的長度,例子中我們使用的時5,這時候切片的容量也是5。當然我們也可以單獨指定切片的容量。

slice:=make([]int,5,10)n

這時,我們創建的切片長度是5,容量是10,需要注意的這個容量10其實對應的是切片底層數組的。

因為切片的底層是數組,所以創建切片時,如果不指定字面值的話,默認值就是數組的元素的零值。這裡我們所以指定了容量是10,但是我們職能訪問5個元素,因為切片的長度是5,剩下的5個元素,需要切片擴充後才可以訪問。

容量必須>=長度,我們是不能創建長度大於容量的切片的。

還有一種創建切片的方式,是使用字面量,就是指定初始化的值。

slice:=[]int{1,2,3,4,5}n

有沒有發現,是創建數組非常像,只不過不用制定[]中的值,這時候切片的長度和容量是相等的,並且會根據我們指定的字面量推導出來。當然我們也可以像數組一樣,只初始化某個索引的值:

slice:=[]int{4:1}n

這是指定了第5個元素為1,其他元素都是默認值0。這時候切片的長度和容量也是一樣的。這裡再次強調一下切片和數組的微小差別。

//數組narray:=[5]int{4:1}n//切片nslice:=[]int{4:1}n

切片還有nil切片和空切片,它們的長度和容量都是0,但是它們指向底層數組的指針不一樣,nil切片意味著指向底層數組的指針為nil,而空切片對應的指針是個地址。

//nil切片nvar nilSlice []intn//空切片nslice:=[]int{}n

nil切片表示不存在的切片,而空切片表示一個空集合,它們各有用處。

切片另外一個用處比較多的創建是基於現有的數組或者切片創建。

slice := []int{1, 2, 3, 4, 5}nslice1 := slice[:]nslice2 := slice[0:]nslice3 := slice[:5]nnfmt.Println(slice1)nfmt.Println(slice2)nfmt.Println(slice3)n

基於現有的切片或者數組創建,使用[i:j]這樣的操作符即可,她表示以i索引開始,到j索引結束,截取原數組或者切片,創建而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。對比Java的話,發現和String的subString方法很像。

i如果省略,默認是0;j如果省略默認是原數組或者切片的長度,所以例子中的三個新切片的值是一樣的。這裡注意的是i和j都不能超過原切片或者數組的索引。

slice := []int{1, 2, 3, 4, 5}nnewSlice := slice[1:3]nnnewSlice[0] = 10nnfmt.Println(slice)nfmt.Println(newSlice)n

這個例子證明了,新的切片和原切片共用的是一個底層數組,所以當修改的時候,底層數組的值就會被改變,所以原切片的值也改變了。當然對於基於數組的切片也一樣的。

我們基於原數組或者切片創建一個新的切片後,那麼新的切片的大小和容量是多少呢?這裡有個公式:

對於底層數組容量是k的切片slice[i:j]來說n長度:j-in容量:k-in

比如我們上面的例子slice[1:3],長度就是3-1=2,容量是5-1=4。不過代碼中我們計算的時候不用這麼麻煩,因為Go語言為我們提供了內置的len和cap函數來計算切片的長度和容量。

slice := []int{1, 2, 3, 4, 5}nnewSlice := slice[1:3]nnfmt.Printf("newSlice長度:%d,容量:%d",len(newSlice),cap(newSlice))n

以上基於一個數組或者切片使用2個索引創建新切片的方法,此外還有一種3個索引的方法,第3個用來限定新切片的容量,其用法為slice[i:j:k]。

slice := []int{1, 2, 3, 4, 5}nnewSlice := slice[1:2:3]n

這樣我們就創建了一個長度為2-1=1,容量為3-1=2的新切片,不過第三個索引,不能超過原切片的最大索引值5。

使用切片

使用切片,和使用數組一樣,通過索引就可以獲取切片對應元素的值,同樣也可以修改對應元素的值。

slice := []int{1, 2, 3, 4, 5}nfmt.Println(slice[2]) //獲取值nslice[2] = 10 //修改值nfmt.Println(slice[2]) //輸出10n

切片只能訪問到其長度內的元素,訪問超過長度外的元素,會導致運行時異常,與切片容量關聯的元素只能用於切片增長。

我們前面講了,切片算是一個動態數組,所以它可以按需增長,我們使用內置append函數即可。append函數可以為一個切片追加一個元素,至於如何增加、返回的是原切片還是一個新切片、長度和容量如何改變這些細節,append函數都會幫我們自動處理。

slice := []int{1, 2, 3, 4, 5}nnewSlice := slice[1:3]nnnewSlice=append(newSlice,10)nfmt.Println(newSlice)nfmt.Println(slice)nn//Outputn[2 3 10]n[1 2 3 10 5]n

例子中,通過append函數為新創建的切片newSlice,追加了一個元素10,我們發現列印的輸出,原切片slice的第4個值也被改變了,變成了10。引起這種結果的原因是因為newSlice有可用的容量,不會創建新的切片來滿足追加,所以直接在newSlice後追加了一個元素10,因為newSlice和slice切片共用一個底層數組,所以切片slice的對應的元素值也被改變了。

這裡newSlice新追加的第3個元素,其實對應的是slice的第4個元素,所以這裡的追加其實是把底層數組的第4個元素修改為10,然後把newSlice長度調整為3。

如果切片的底層數組,沒有足夠的容量時,就會新建一個底層數組,把原來數組的值複製到新底層數組裡,再追加新值,這時候就不會影響原來的底層數組了。

所以一般我們在創建新切片的時候,最好要讓新切片的長度和容量一樣,這樣我們在追加操作的時候就會生成新的底層數組,和原有數組分離,就不會因為共用底層數組而引起奇怪問題,因為共用數組的時候修改內容,會影響多個切片。

append函數會智能的增長底層數組的容量,目前的演算法是:容量小於1000個時,總是成倍的增長,一旦容量超過1000個,增長因子設為1.25,也就是說每次會增加25%的容量。

內置的append也是一個可變參數的函數,所以我們可以同時追加好幾個值。

newSlice=append(newSlice,10,20,30)n

此外,我們還可以通過...操作符,把一個切片追加到另一個切片里。

slice := []int{1, 2, 3, 4, 5}nnewSlice := slice[1:2:3]nnnewSlice=append(newSlice,slice...)nfmt.Println(newSlice)nfmt.Println(slice)n

迭代切片

切片是一個集合,我們可以使用 for range 循環來迭代它,列印其中的每個元素以及對應的索引。

slice := []int{1, 2, 3, 4, 5}n for i,v:=range slice{n fmt.Printf("索引:%d,值:%dn",i,v)n }n

如果我們不想要索引,可以使用_來忽略它,這是Go語言的用法,很多不需要的函數等返回值,都可以忽略。

slice := []int{1, 2, 3, 4, 5}n for _,v:=range slice{n fmt.Printf("值:%dn",v)n }n

這裡需要說明的是range返回的是切片元素的複製,而不是元素的引用。

除了for range循環外,我們也可以使用傳統的for循環,配合內置的len函數進行迭代。

slice := []int{1, 2, 3, 4, 5}n for i := 0; i < len(slice); i++ {n fmt.Printf("值:%dn", slice[i])n }n

在函數間傳遞切片

我們知道切片是3個欄位構成的結構類型,所以在函數間以值的方式傳遞的時候,佔用的內存非常小,成本很低。在傳遞複製切片的時候,其底層數組不會被複制,也不會受影響,複製只是複製的切片本身,不涉及底層數組。

func main() {n slice := []int{1, 2, 3, 4, 5}n fmt.Printf("%pn", &slice)n modify(slice)n fmt.Println(slice)n}nnfunc modify(slice []int) {n fmt.Printf("%pn", &slice)n slice[1] = 10n}n

列印的輸出如下:

0xc420082060n0xc420082080n[1 10 3 4 5]n

仔細看,這兩個切片的地址不一樣,所以可以確認切片在函數間傳遞是複製的。而我們修改一個索引的值後,發現原切片的值也被修改了,說明它們共用一個底層數組。

在函數間傳遞切片非常高效,而且不需要傳遞指針和處理複雜的語法,只需要複製切片,然後根據自己的業務修改,最後傳遞迴一個新的切片副本即可,這也是為什麼函數間傳遞參數,使用切片,而不是數組的原因。

關於多維切片就不介紹了,還有多維數組,一來它和普通的切片數組一樣,只不過是多個一維組成的多維;二來我壓根不推薦用多維切片和數組,可讀性不好,結構不夠清晰,容易出問題。

《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org,第一時間看後續筆記。

推薦閱讀:

TAG:Go语言 | golang最佳实践 | 编程语言 |