Skip to content

切片

在 Go 中,數組和切片兩者看起來長得幾乎一模一樣,但功能有著不小的區別,數組是定長的數據結構,長度被指定後就不能被改變,而切片是不定長的,切片在容量不夠時會自行擴容。

數組

如果事先就知道了要存放數據的長度,且後續使用中不會有擴容的需求,就可以考慮使用數組,Go 中的數組是值類型,而非引用,並不是指向頭部元素的指針。

TIP

數組作為值類型,將數組作為參數傳遞給函數時,由於 Go 函數是傳值傳遞,所以會將整個數組拷貝。

初始化

數組在聲明時長度只能是一個常量,不能是變量,你不能在聲明一個變量讓後用變量作為數組的長度值

go
// 正確示例
var a [5]int

// 錯誤示例
l := 1
var b [l]int

先來初始化一個長度為 5 的整型數組

go
var nums [5]int

也可以用元素初始化

go
nums := [5]int{1, 2, 3}

可以讓編譯器自動推斷長度

go
nums := [...]int{1, 2, 3, 4, 5} //等價於nums := [5]int{1, 2, 3, 4, 5},省略號必須存在,否則生成的是切片,不是數組

還可以通過new函數獲得一個指針

go
nums := new([5]int)

以上幾種方式都會給nums分配一片固定大小的內存,區別只是最後一種得到的值是指針。

在數組初始化時,需要注意的是,長度必須為一個常量表達式,否則將無法通過編譯,常量表達式即表達式的最終結果是一個常量,錯誤例子如下:

go
length := 5 // 這是一個變量
var nums [length]int

length是一個變量,因此無法用於初始化數組長度,如下是正確示例:

go
const length = 5
var nums [length]int // 常量
var nums2 [length + 1]int // 常量表達式
var nums3 [(1 + 2 + 3) * 5]int // 常量表達式
var nums4 [5]int // 最常用的

使用

只要有數組名和下標,就可以訪問數組中對應的元素。

go
fmt.Println(nums[0])

同樣的也可以修改數組元素

go
nums[0] = 1

還可以通過內置函數len來訪問數組元素的數量

go
len(nums)

內置函數cap來訪問數組容量,數組的容量等於數組長度,容量對於切片才有意義。

go
cap(nums)

切割

切割數組的格式為arr[startIndex:endIndex],切割的區間為左閉右開。且數組在切割後,就會變為切片類型。例子如下:

go
nums := [5]int{1, 2, 3, 4, 5}

nums[:] // 子切片范圍[0,5) -> [1 2 3 4 5]
nums[1:] // 子切片范圍[1,5) -> [2 3 4 5]
nums[:5] // 子切片范圍[0,5) -> [1 2 3 4 5]
nums[2:3] // 子切片范圍[2,3) -> [3]
nums[1:3] // 子切片范圍[1,3) -> [2 3]
go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  fmt.Printf("%T\n", arr)
  fmt.Printf("%T\n", arr[1:2])
}

輸出

[5]int
[]int

若要將數組轉換為切片類型,不帶參數進行切片即可,轉換後的切片與原數組指向的是同一片內存,修改切片會導致原數組內容的變化

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := arr[:]
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

輸出

array: [0 2 3 4 5]
slice: [0 2 3 4 5]

如果要對轉換後的切片進行修改,建議使用下面這種方式進行轉換

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := slices.Clone(arr[:])
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

輸出

array: [1 2 3 4 5]
slice: [0 2 3 4 5]

切片

切片在 Go 中的應用范圍要比數組廣泛的多,它用於存放不知道長度的數據,且後續使用過程中可能會頻繁的插入和刪除元素。

初始化

切片的初始化方式有以下幾種

go
var nums []int // 值
nums := []int{1, 2, 3} // 值
nums := make([]int, 0, 0) // 值
nums := new([]int) // 指針

可以看到切片與數組在外貌上的區別,僅僅只是少了一個初始化長度。通常情況下,推薦使用make來創建一個空切片,只是對於切片而言,make函數接收三個參數:類型,長度,容量。舉個例子解釋一下長度與容量的區別,假設有一桶水,水並不是滿的,桶的高度就是桶的容量,代表著總共能裝多少高度的水,而桶中水的高度就是代表著長度,水的高度一定小於等於桶的高度,否則水就溢出來了。所以,切片的長度代表著切片中元素的個數,切片的容量代表著切片總共能裝多少個元素,切片與數組最大的區別在於切片的容量會自動擴張,而數組不會,更多細節前往參考手冊 - 長度與容量

TIP

切片的底層實現依舊是數組,是引用類型,可以簡單理解為是指向底層數組的指針(本質上切片在Go中是一個結構體,包含指向底層數組的指針、長度值、容量值)。因此切片作為函數參數傳遞時不復制底層數組,函數內對傳入切片的修改會反映在原切片中。

通過var nums []int這種方式聲明的切片,默認值為nil,所以不會為其分配內存,而在使用make進行初始化時,建議預分配一個足夠的容量,可以有效減少後續擴容的內存消耗。

使用

切片的基本使用與數組完全一致,區別只是切片可以動態變化長度,下面看幾個例子。

切片可以通過append函數實現許多操作,函數簽名如下,slice是要添加元素的目標切片,elems是待添加的元素,返回值是添加後的切片。

go
func append(slice []Type, elems ...Type) []Type

首先創建一個長度為 0,容量為 0 的空切片,然後在尾部插入一些元素,最後輸出長度和容量。

go
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 可以看到長度與容量並不一致。

新 slice 預留的 buffer 容量 大小是有一定規律的。 在 golang1.18 版本更新之前網上大多數的文章都是這樣描述 slice 的擴容策略的: 當原 slice 容量小於 1024 的時候,新 slice 容量變成原來的 2 倍;原 slice 容量超過 1024,新 slice 容量變成原來的 1.25 倍。 在 1.18 版本更新之後,slice 的擴容策略變為了: 當原 slice 容量(oldcap)小於 256 的時候,新 slice(newcap)容量為原來的 2 倍;原 slice 容量超過 256,新 slice 容量 newcap = oldcap+(oldcap+3*256)/4

插入元素

切片元素的插入也是需要結合append函數來使用,現有切片如下,

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

從頭部插入元素

go
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

從中間下標 i 插入元素

go
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]

從尾部插入元素,就是append最原始的用法

go
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

刪除元素

切片元素的刪除需要結合append函數來使用,現有如下切片

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

從頭部刪除 n 個元素

go
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

從尾部刪除 n 個元素

go
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

從中間指定下標 i 位置開始刪除 n 個元素

go
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]

刪除所有元素

go
nums = nums[:0]
fmt.Println(nums) // []

拷貝

切片在拷貝時需要確保目標切片有足夠的長度,例如

go
func main() {
  dest := make([]int, 0)
  src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  fmt.Println(src, dest)
  fmt.Println(copy(dest, src))
  fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []

將長度修改為 10,輸出如下

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]

遍歷

切片的遍歷與數組完全一致,for循環

go
func main() {
   slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
   for i := 0; i < len(slice); i++ {
      fmt.Println(slice[i])
   }
}

for range循環

go
func main() {
  slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
  for index, val := range slice {
    fmt.Println(index, val)
  }
}

多維切片

先來看下面的一個例子,官方文檔也有解釋:Effective Go - 二維切片

go
var nums [5][5]int
for _, num := range nums {
   fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
   fmt.Println(slice)
}

輸出結果為

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[]
[]
[]
[]
[]

可以看到,同樣是二維的數組和切片,其內部結構是不一樣的。數組在初始化時,其一維和二維的長度早已固定,而切片的長度是不固定的,切片中的每一個切片長度都可能是不相同的,所以必須要單獨初始化,切片初始化部分修改為如下代碼即可。

go
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
   slices[i] = make([]int, 5)
}

最終輸出結果為

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

拓展表達式

TIP

只有切片才能使用拓展表達式

切片與數組都可以使用簡單表達式來進行切割,但是拓展表達式只有切片能夠使用,該特性於 Go1.2 版本添加,主要是為了解決切片共享底層數組的讀寫問題,主要格式為如下,需要滿足關系low<= high <= max <= cap,使用拓展表達式切割的切片容量為max-low

go
slice[low:high:max]

lowhigh依舊是原來的含義不變,而多出來的max則指的是最大容量,例如下方的例子中省略了max,那麼s2的容量就是cap(s1)-low

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

那麼這麼做就會有一個明顯的問題,s1s2是共享的同一個底層數組,在對s2進行讀寫時,有可能會影響的s1的數據,下列代碼就屬於這種情況

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
s2 = append(s2, 1)   // 添加新元素,由於容量為6.所以沒有擴容,直接修改底層數組
fmt.Println(s2)
fmt.Println(s1)

最終的輸出為

[4 1]
[1 2 3 4 1 6 7 8 9]

可以看到明明是向s2添加元素,卻連s1也一起修改了,拓展表達式就是為了解決此類問題而生的,只需要稍微修改一下就能解決該問題

go
func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   s2 = append(s2, 1)    // 容量不足,分配新的底層數組
   fmt.Println(s2)
   fmt.Println(s1)
}

現在得到的結果就是正常的

[4 1]
[1 2 3 4 5 6 7 8 9]

clear

在 go1.21 新增了clear內置函數,clear 會將切片內所有的值置為零值,

go
package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

輸出

[0 0 0 0]

如果想要清空切片,可以

go
func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

限制了切割後的容量,這樣可以避免覆蓋原切片的後續元素。

Golang學習網由www.golangdev.cn整理維護