go slice使用

落雷發表於2023-04-27

1. 簡介

在go中,slice是一種動態陣列型別,其底層實現中使用了陣列。slice有以下特點:

*slice本身並不是陣列,它只是一個引用型別,包含了一個指向底層陣列的指標,以及長度和容量。
*slice的長度可以動態擴充套件或縮減,透過appendcopy操作可以增加或刪除slice中的元素。
*slice的容量是指在底層陣列中slice可以繼續擴充套件的長度,容量可以透過make函式進行設定。

Slice 的底層實現是一個包含了三個欄位的結構體:

type`slice`struct {
    ptr uintptr // 指向底層陣列的指標
    len int     // slice 的長度
    cap int     // slice 的容量
}

當一個新的slice被建立時,Go會為其分配一個底層陣列,並且把指向該陣列的指標、長度和容量資訊儲存在slice結構體中。底層陣列的長度一般會比slice的容量要大,以便在append操作時有足夠的空間儲存新元素。

當一個slice作為引數傳遞給函式時,其實是傳遞了一個指向底層陣列的指標,這也就意味著在函式內部對slice的修改也會反映到函式外部。

在進行切片操作時,slice 的指標和長度資訊不會發生變化,只有容量資訊會發生變化。如果切片操作的結果仍然是一個 slice,那麼它所引用的底層陣列仍然和原來的slice是同一個陣列。

需要注意的是,當一個slice被傳遞給一個新的變數或者作為引數傳遞給函式時,並不會複製底層陣列,而是會共享底層陣列。因此,如果對一個slice的元素進行修改,可能會影響到共享底層陣列的其他slice。如果需要複製一個slice,可以使用copy函式。

2. 使用

slice的使用包括定義初始化新增刪除查詢等操作。

2.1 slice定義

slice是一個引用型別,可以透過宣告變數並使用make()函式來建立一個slice

var sliceName []T
sliceName := make([]T, length, capacity)

其中,T代表該切片可以儲存的元素型別,length代表預留的元素數量,capacity代表預分配的儲存空間。

2.2 初始化

slice有兩種初始化的方式:宣告時初始化和使用append()函式初始化:

// 宣告時初始化
sliceName := []T{value1, value2, ..., valueN}

// 使用append()函式進行初始化
sliceName := make([]T, 0, capacity)
sliceName = append(sliceName, value1, value2, ..., valueN)

2.3 獲取slice元素

slice中的元素可以透過索引的方式來獲取,與c/c++類似,go的索引也是從0開始的:

sliceName[index]

2.4 新增元素到slice中

可以透過使用append()函式將元素新增到slice中。如果slice的容量不足,則會自動擴充套件。語法如下:

sliceName = append(sliceName, value1, value2, ..., valueN)

2.5 刪除slice中的元素

可以使用append()函式和切片操作來從slice中刪除元素。使用append()函式時,需要將帶有要刪除元素的切片放在最後。語法如下:

// 透過切片操作刪除元素
sliceName = append(sliceName[:index], sliceName[index+1:]...)

// 透過append()函式刪除元素
sliceName = append(sliceName[:index], sliceName[index+1:]...)

如上所見,二者的表現形式是一樣的,但內部實現是不同的:

  • 使用append()進行刪除的方式,實際上是將後面的元素向前移動一個位置,然後透過重新切片的方式來刪除最後一個元素。這種方式會建立一個新的底層陣列,並將原來的元素複製到新的陣列中,因此在刪除多個元素時可能會導致記憶體分配和複製開銷較大,影響效能
  • 使用切片語法進行刪除,底層陣列中被刪除元素的位置仍然存在,但是這些位置不再包含有效的資料。這種方式的效能比使用append()進行刪除要好,尤其是在刪除多個元素時,因為它不需要建立新的底層陣列,也不需要複製元素。但是,這種方式可能會導致底層陣列中存在大量未使用的空間,浪費記憶體

需要注意的是,在切片中刪除元素時,會重新分配記憶體並複製元素,因此刪除元素的成本會相對較高。為了減少記憶體分配和複製元素的次數,可以使用copy函式將後面的元素複製到前面,然後將切片的長度減少。具體實現方法可以參考下面的:

// 刪除切片中指定位置的元素
func removeElement(slice []int, index int) []int {
    copy(slice[index:], slice[index+1:])
    return slice[:len(slice)-1]
}

2.6 查詢slice中的元素

可以使用forrange遍歷slice來實現元素查詢:

// 使用for迴圈和range關鍵字遍歷Slice
for index, value := range sliceName {
    if value == targetValue {
        // 找到了目標元素
        break
    }
}

2.7 切片操作

可以使用切片操作來獲取子切片,操作如下:

// 切片操作:獲取從第i個元素到第j個元素的子切片
sliceName[i:j]

// 切片操作:獲取從第i個元素到第j個元素,且容量為k的子切片
sliceName[i:j:k]

3. 關於slice擴容

在Go語言中,slice會隨著元素的增加而動態擴容。當容量不足時,slice會自動重新分配記憶體,將原有元素複製到新的底層陣列中,並在新陣列後面新增新的元素。

slice的擴容機制可以描述為:當slice的長度超過了底層陣列的容量時,Go語言會按照一定的策略重新分配一塊更大的記憶體,並將原來的元素複製到新的記憶體中,然後再新增新元素。具體的策略如下:

  1. 如果新長度(即len(s)+1)小於等於原長度(即cap(s)),則slice不需要擴容,直接新增元素即可。
  2. 如果新長度大於原長度且小於原長度的兩倍(即 cap(s)*2),則新slice的容量就是原來的兩倍,也就是說將底層陣列擴容為原來的兩倍,並將原來的元素複製到新的陣列中。
  3. 如果新長度大於原長度的兩倍,會嘗試使用新長度作為容量,如果仍然不夠,則按照擴容倍數(預設是 2)來擴容。

需要注意的是,slice擴容是一個開銷比較大的操作,因為需要重新分配記憶體、複製資料等。所以在編寫程式碼時應該儘可能地減少slice擴容的次數,以提高程式的效能。


宣告:本作品採用署名-非商業性使用-相同方式共享 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意


相關文章