1. 程式人生 > 其它 >go切片的nil 切片、空切片與零切片(重要)

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 語 言 編 程]
  • 特殊操作
  1. 追加新元素(擴容操作)

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 增加。

  1. 刪除切片元素(縮容操作)

前面說到切片是動態的陣列,靈活又方便,既能使用 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}

通過上面的程式碼,我們得出一個結論:從切片尾部刪除元素不會觸發縮容,僅長度發生變化,從切片頭部刪除元素則會觸發縮容,長度及容量都發生變化。所以刪除的元素不再使用後,一般建議申請新的記憶體空間,建立新的切片來接收要保留的元素,這樣可以避免原底層陣列記憶體無效佔用,從源切片頭部刪除不存在此問題。

  1. 拷貝切片

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() 函式更勝一籌。

說到拷貝,它可分為深拷貝與淺拷貝,我們一般預設值型別的資料是深拷貝,引用型別的資料是淺拷貝,那深拷貝與淺拷貝有什麼區別呢?深拷貝是指使用資料時,基於原始資料建立一個副本,有獨立的底層資料空間,之後的操作都在副本上進行,對原始資料沒有任何影響。反之,操作對原始資料有影響的叫做淺拷貝,比如拷貝原始陣列地址。我們可以回想一下,在前面的切片操作中,哪些是深拷貝,哪些是淺拷貝。

  1. 判斷 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 語言學習之路