這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org
,第一時間看後續筆記。
切片也是一種資料結構,它和數組非常相似,因為他是圍繞動態數組的概念設計的,可以按需自動改變大小,使用這種結構,可以更方便的管理和使用資料集合。
內部實現
切片是基於數組實現的,它的底層是數組,它自己本身非常小,可以理解為對底層數組的抽象。因為機遇數組實現,所以它的底層的記憶體是連續非配的,效率非常高,還可以通過索引獲得資料,可以迭代以及記憶體回收最佳化的好處。
切片對象非常小,是因為它是只有3個欄位的資料結構:一個是指向底層數組的指標,一個是切片的長度,一個是切片的容量。這3個欄位,就是Go語言操作底層數組的中繼資料,有了它們,我們就可以任意的操作切片了。
聲明和初始化
切片建立的方式有好幾種,我們先看下最簡潔的make方式。
使用內建的make
函數時,需要傳入一個參數,指定切片的長度,例子中我們使用的時5,這時候切片的容量也是5。當然我們也可以單獨指定切片的容量。
1 |
slice:=make([]int,5,10) |
這時,我們建立的切片長度是5,容量是10,需要注意的這個容量10其實對應的是切片底層數組的。
因為切片的底層是數組,所以建立切片時,如果不指定字面值的話,預設值就是數組的元素的零值。這裡我們所以指定了容量是10,但是我們職能訪問5個元素,因為切片的長度是5,剩下的5個元素,需要切片擴充後才可以訪問。
容量必須>=長度,我們是不能建立長度大於容量的切片的。
還有一種建立切片的方式,是使用字面量,就是指定初始化的值。
1 |
slice:=[]int{1,2,3,4,5} |
有沒有發現,是建立數組非常像,只不過不用制定[]
中的值,這時候切片的長度和容量是相等的,並且會根據我們指定的字面量推匯出來。當然我們也可以像數組一樣,只初始化某個索引的值:
這是指定了第5個元素為1,其他元素都是預設值0。這時候切片的長度和容量也是一樣的。這裡再次強調一下切片和數組的微小差別。
1234 |
//數組array:=[5]int{4:1}//切片slice:=[]int{4:1} |
切片還有nil切片和空切片,它們的長度和容量都是0,但是它們指向底層數組的指標不一樣,nil切片意味著指向底層數組的指標為nil,而空切片對應的指標是個地址。
1234 |
//nil切片var nilSlice []int//空切片slice:=[]int{} |
nil切片表示不存在的切片,而空切片表示一個空集合,它們各有用處。
切片另外一個用處比較多的建立是基於現有的數組或者切片建立。
12345678 |
slice := []int{1, 2, 3, 4, 5}slice1 := slice[:]slice2 := slice[0:]slice3 := slice[:5]fmt.Println(slice1)fmt.Println(slice2)fmt.Println(slice3) |
基於現有的切片或者數組建立,使用[i:j]
這樣的操作符即可,她表示以i
索引開始,到j
索引結束,截取原數組或者切片,建立而成的新切片,新切片的值包含原切片的i
索引,但是不包含j
索引。對比Java的話,發現和String的subString
方法很像。
i
如果省略,預設是0;j
如果省略預設是原數組或者切片的長度,所以例子中的三個新切片的值是一樣的。這裡注意的是i
和j
都不能超過原切片或者數組的索引。
1234567 |
slice := []int{1, 2, 3, 4, 5}newSlice := slice[1:3]newSlice[0] = 10fmt.Println(slice)fmt.Println(newSlice) |
這個例子證明了,新的切片和原切片共用的是一個底層數組,所以當修改的時候,底層數組的值就會被改變,所以原切片的值也改變了。當然對於基於數組的切片也一樣的。
我們基於原數組或者切片建立一個新的切片後,那麼新的切片的大小和容量是多少呢?這裡有個公式:
123 |
對於底層數組容量是k的切片slice[i:j]來說長度:j-i容量:k-i |
比如我們上面的例子slice[1:3]
,長度就是3-1=2
,容量是5-1=4
。不過代碼中我們計算的時候不用這麼麻煩,因為Go語言為我們提供了內建的len
和cap
函數來計算切片的長度和容量。
1234 |
slice := []int{1, 2, 3, 4, 5}newSlice := slice[1:3]fmt.Printf("newSlice長度:%d,容量:%d",len(newSlice),cap(newSlice)) |
以上基於一個數組或者切片使用2個索引建立新切片的方法,此外還有一種3個索引的方法,第3個用來限定新切片的容量,其用法為slice[i:j:k]
。
12 |
slice := []int{1, 2, 3, 4, 5}newSlice := slice[1:2:3] |
這樣我們就建立了一個長度為2-1=1
,容量為3-1=2
的新切片,不過第三個索引,不能超過原切片的最大索引值5。
使用切片
使用切片,和使用數組一樣,通過索引就可以擷取切片對應元素的值,同樣也可以修改對應元素的值。
1234 |
slice := []int{1, 2, 3, 4, 5}fmt.Println(slice[2]) //擷取值slice[2] = 10 //修改值fmt.Println(slice[2]) //輸出10 |
切片只能訪問到其長度內的元素,訪問超過長度外的元素,會導致運行時異常,與切片容量關聯的元素只能用於切片增長。
我們前面講了,切片算是一個動態數組,所以它可以按需增長,我們使用內建append
函數即可。append
函數可以為一個切片追加一個元素,至於如何增加、返回的是原切片還是一個新切片、長度和容量如何改變這些細節,append
函數都會幫我們自動處理。
12345678910 |
slice := []int{1, 2, 3, 4, 5}newSlice := slice[1:3]newSlice=append(newSlice,10)fmt.Println(newSlice)fmt.Println(slice)//Output[2 3 10][1 2 3 10 5] |
例子中,通過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
也是一個可變參數的函數,所以我們可以同時追加好幾個值。
1 |
newSlice=append(newSlice,10,20,30) |
此外,我們還可以通過...
操作符,把一個切片追加到另一個切片裡。
123456 |
slice := []int{1, 2, 3, 4, 5}newSlice := slice[1:2:3]newSlice=append(newSlice,slice...)fmt.Println(newSlice)fmt.Println(slice) |
迭代切片
切片是一個集合,我們可以使用 for range 迴圈來迭代它,列印其中的每個元素以及對應的索引。
1234 |
slice := []int{1, 2, 3, 4, 5}for i,v:=range slice{fmt.Printf("索引:%d,值:%d\n",i,v)} |
如果我們不想要索引,可以使用_
來忽略它,這是Go語言的用法,很多不需要的函數等傳回值,都可以忽略。
1234 |
slice := []int{1, 2, 3, 4, 5}for _,v:=range slice{fmt.Printf("值:%d\n",v)} |
這裡需要說明的是range
返回的是切片元素的複製,而不是元素的引用。
除了for range迴圈外,我們也可以使用傳統的for迴圈,配合內建的len函數進行迭代。
1234 |
slice := []int{1, 2, 3, 4, 5}for i := 0; i < len(slice); i++ {fmt.Printf("值:%d\n", slice[i])} |
在函數間傳遞切片
我們知道切片是3個欄位構成的結構類型,所以在函數間以值的方式傳遞的時候,佔用的記憶體非常小,成本很低。在傳遞複製切片的時候,其底層數組不會被複製,也不會受影響,複製只是複製的切片本身,不涉及底層數組。
1234567891011 |
func main() {slice := []int{1, 2, 3, 4, 5}fmt.Printf("%p\n", &slice)modify(slice)fmt.Println(slice)}func modify(slice []int) {fmt.Printf("%p\n", &slice)slice[1] = 10} |
列印的輸出如下:
123 |
0xc4200820600xc420082080[1 10 3 4 5] |
仔細看,這兩個切片的地址不一樣,所以可以確認切片在函數間傳遞是複製的。而我們修改一個索引的值後,發現原切片的值也被修改了,說明它們共用一個底層數組。
在函數間傳遞切片非常高效,而且不需要傳遞指標和處理複雜的文法,只需要複製切片,然後根據自己的業務修改,最後傳遞迴一個新的切片副本即可,這也是為什麼函數間傳遞參數,使用切片,而不是數組的原因。
關於多維切片就不介紹了,還有多維陣列,一來它和普通的切片數組一樣,只不過是多個一維組成的多維;二來我壓根不推薦用多維切片和數組,可讀性不好,結構不夠清晰,容易出問題。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org
,第一時間看後續筆記。