go切片的nil 切片、空切片與零切片(重要)
nil
切片、空切片與零切片是切片的三種狀態,nil
切片是指在宣告時未做初始化的切片,不用分配記憶體空間,一般使用 var
建立。使用 make
建立的空切片需要分配記憶體空間,nil
切片與空切片的長度、容量都為 0 ,如果我們要建立長度容量為 0 的切片,官方推薦 nil
切片。零切片指初始值為型別零值的切片。
// 建立 nil 切片
var slice []int
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:[] {0 0 0}
// 建立空切片
slice2 := make([]int,0)
slice3 := []int{}
fmt.Println(slice2,*(*reflect.SliceHeader)(unsafe.Pointer(&slice2))) // 輸出:[] {18504816 0 0}
fmt.Println(slice3,*(*reflect.SliceHeader)(unsafe.Pointer(&slice3))) // 輸出:[] {18504816 0 0}
// 建立零切片
slice4 := make([]int,2,5)
fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 輸出:[0 0] {824634474496 2 5}
- 基本操作
slice := []string{"Go","語","言","編","程"}
// 1.訪問元素
fmt.Println(slice[0]) // 輸出:Go
// 2.修改元素
slice[0] = "PHP"
fmt.Println(slice) // 輸出:[PHP 語 言 編 程]
- 特殊操作
- 追加新元素(擴容操作)
append()
函式 是 Go 語言裡專為切片型別提供的操作函式。切片長度不足時,使用 append()
函式,可在切片頭部或尾部追加新元素,但是,由於在頭部追加元素會導致記憶體重新分配,所有元素將複製一次,因此大多數情況下推薦在尾部追加。追加的新元素可以是一個或多個,甚至是一個切片。append()
slice := []string{"Go","語","言","編","程"}
// 1.在切片尾部追加一個元素
slice = append(slice, "!")
fmt.Println(slice,len(slice), cap(slice)) // 輸出:[Go 語 言 編 程 !] 6 10
// 2.在切片尾部追加多個元素
slice = append(slice,"!","!")
fmt.Println(slice,len(slice), cap(slice)) // 輸出:[Go 語 言 編 程 ! ! !] 8 10
// 3.在切片尾部追加切片
slice = append(slice,[]string{"!","!"}...)
fmt.Println(slice,len(slice), cap(slice)) // 輸出:[Go 語 言 編 程 ! ! ! ! !] 10 10
// 4.在切片頭部追加切片
slice = append([]string{"最","愛"},slice...)
fmt.Println(slice,len(slice), cap(slice)) // 輸出:[最 愛 Go 語 言 編 程 ! ! ! ! !] 12 12
一個數組包含型別與長度兩部分,切片的底層是一個數組,切片容量等於陣列長度,在這個前提下,我們建立切片並修改它的長度時,如果長度小於容量(陣列長度),那麼函式將直接在原底層陣列上增加新的元素,如果切片長度超出容量(陣列長度),append()
函式就會建立一個新的底層陣列,再將源陣列的值複製過來,我們可以通過對比 SliceHeader.Data
欄位觀察底層陣列發生的變化。
// 發生擴容前
slice := []string{"Go","語","言","編","程"}
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:{824634458112 5 5}
// 發生擴容後
slice = append(slice, "!")
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:{824634466464 6 10}
append()
擴容邏輯大概是這樣,當 size 小於 1024 位元組時,按乘以 2 的長度建立新的底層陣列,超過 1024 位元組時,按 1/4 增加。
- 刪除切片元素(縮容操作)
前面說到切片是動態的陣列,靈活又方便,既能使用 append()
函式對它進行擴容,也能使用 [:]
運算子在源切片上建立新的切片,實現元素的刪除。:
左邊是開始位,右邊是結束位,它們表示元素開始與結束的選取範圍。
// 建立一個空切片
slice := make([]string,0)
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:[] {18541680 0 0}
// 使用 append 函式在切片尾部追加一個切片,觸發擴容,記憶體重新分配
slice = append(slice,[]string{"最","愛","Go","語","言","編","程","!","!"}...)
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:[最 愛 Go 語 言 編 程 ! !] {824634204160 8 9}
// 從切片尾部刪除元素,賦值給新切片,新切片與源切片共享同一個底層陣列
newSlice := slice[0:8]
fmt.Println(newSlice,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice))) // 輸出:[最 愛 Go 語 言 編 程 !] {824634204160 8 9}
// 從切片尾部刪除元素,建立新切片和新的底層陣列
var newSlice2 []string
newSlice2 = append(newSlice2,slice[0:8]...)
fmt.Println(newSlice2,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice2))) // 輸出:[最 愛 Go 語 言 編 程 !] {824634212352 8 8}
// 從切片頭部和尾部刪除元素並返回新切片,記憶體重新分配
slice = slice[2:7]
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 輸出:[Go 語 言 編 程] {824634204192 5 7}
通過上面的程式碼,我們得出一個結論:從切片尾部刪除元素不會觸發縮容,僅長度發生變化,從切片頭部刪除元素則會觸發縮容,長度及容量都發生變化。所以刪除的元素不再使用後,一般建議申請新的記憶體空間,建立新的切片來接收要保留的元素,這樣可以避免原底層陣列記憶體無效佔用,從源切片頭部刪除不存在此問題。
- 拷貝切片
Go 語言提供了內建函式 copy()
用來拷貝切片。在前面的程式碼裡用 append()
函式演示瞭如何從切片頭部或尾部刪除元素,那如果想從中間刪除應該怎麼做?還有,前面說到當切片容量不足以新增新元素時,append()
函式會依據規則建立新的大容量底層陣列,在此基礎建立新切片,再將源切片的內容拷貝到新切片中,那它是如何操作的呢?
// 源切片
slice := []string{"Go","語","言","言","言","編","程","!"}
// 建立新切片
newSlice := make([]string,6)
// 選擇範圍拷貝到新切片
at := copy(newSlice,slice[0:3])
copy(newSlice[at:],slice[5:8])
fmt.Println(newSlice) // 輸出:[Go 語 言 編 程 !]
copy()
函式的操作使用 append()
函式也能實現,只不過 append()
函式每次複製資料都需要建立臨時切片,相比效能上 copy()
函式更勝一籌。
說到拷貝,它可分為深拷貝與淺拷貝,我們一般預設值型別的資料是深拷貝,引用型別的資料是淺拷貝,那深拷貝與淺拷貝有什麼區別呢?深拷貝是指使用資料時,基於原始資料建立一個副本,有獨立的底層資料空間,之後的操作都在副本上進行,對原始資料沒有任何影響。反之,操作對原始資料有影響的叫做淺拷貝,比如拷貝原始陣列地址。我們可以回想一下,在前面的切片操作中,哪些是深拷貝,哪些是淺拷貝。
- 判斷 2 個字串切片是否相等
Go 語言標準庫中有專門的方法判斷兩個位元組切片是否相等,卻沒有針對字串切片的,當我們要判斷兩個字串切片是否相等時,可以使用 reflect
包提供的 DeepEqual()
方法,或者自定義。但不管哪種,在使用前都要先定義什麼是相等,一個切片包含型別、長度、容量和元素值,按照以往的經驗,當型別、元素值、長度一致時,我們便認為這兩個切片是相等的。只是,使用自定義方法需要先確定型別再比較,而使用 reflect.DeepEqual()
可以比較兩個不確定型別的值,但是需要付出很大的效能代價,所以通常我們還是使用前者。
// 建立兩個長度相同、容量不同的切片
slice1 := make([]string,2,5)
slice1[0] = "Go"
slice1[1] = "語"
slice2 := make([]string,2,6)
slice2[0] = "Go"
slice2[1] = "語"
// 方法1:自定義方法
status := true
if len(slice1) != len(slice2){
status = false
}else{
for k,_ := range slice1 {
if slice1[k] != slice2[k] {
status = false
}
}
}
fmt.Println(status) // 輸出:true
// 方法2:使用 reflect.DeepEqual
fmt.Println(reflect.DeepEqual(slice1,slice2)) // 輸出:true
總結
陣列跟切片是 Go
語言中很基礎的內容,它們的操作也不僅限於前面介紹的那些。切片在某種程度上是來源於陣列的,所以他們可以支援相同的操作方法,但切片的形式更為動態,操作方法和使用場景也更加豐富。
這次總結 Go
語言陣列跟切片的內容花費了很長時間,儘管如此,仍然有一些”未解之謎“,對於已瞭解的內容也不敢說都理解恰當,我將帶著這些問題,繼續 Go 語言學習之路