Go | 切片(Slice)

切片(Slice)是操作其指向之陣列(Array)的物件。

[指標|長度|最大長度] // 切片
  |
[1,2,3,4,5] // 陣列

切片的初始化

// 宣告一個長度與最大長度都是 5 的字串陣列
slice1 := make([]string, 5)

// 宣告一個長度為 3 最大長度為 5 的整數陣列
slice2 := make([]int, 3, 5)

長度不能大於最大長度,不然編譯器會噴錯誤。一般來說,宣告切片的同時也會同時賦值:

// 宣告一個長度與最大長度為 3 的切片
slice1 := []string{"One", "Two", "Three"}

// 宣告一個長度與最大長度為 100 的切片
slice2 := []string{99: ""}

空切片(nil slice)與空白切片(empty slice)

不管是空切片還是空白切片,標準函式庫裡的 appendlencap 的使用行為都一樣。

空切片(nil slice)

在 Go 程式中,空切片很常出現,通常被當成不存在資料時的回傳值,例如 return nilSlice, error

空切片的宣吿方式:

var slice []int

空切片長成這樣:

位置 0 1 2
用途 指標 長度 最大長度
nil 0 0

空白切片(empty slice)

空白切片通常是用來表達零資料集合,例如查詢資料庫後回傳零筆資料。空白切片的指標指向空陣列,所以並不會另外佔用記憶體。

slice := make([]int, 0)
slice := []int{}

操作切片

slice := []int{100,200,300,400,500}
newSlice := slice[1:3]

看一下記憶體內的樣子:

   slice := []int{100,200,300,400,500}
 [ 指標 | 5 | 5 ]
    |
   [0]     [1]     [2]     [3]     [4]
[  100  |  200  |  300  |  400  |  500  ] // 底層陣列
            |
         [ 指標 | 2 | 4 ]
           newSlice := slice[1:3]

雖然 slicenewSlice 都指向同一個陣列,但這兩個切片的世界觀是不同的。對於一個最大長度為 k 的切片 slice[i:j]:

Length:   j - i
Capacity: k - i

newSlice 而言,陣列裡的第一個元素甚至是不存在的。

需要注意的是,由於切片是指向某個底層陣列,所以如果有其他的切片對於這個陣列有做任何操作,操作結果將會同時反映到其他也指向此陣列的切片。

長度與最大長度

即使最大長度允許,但切片不能存取大於長度的位址。

slice := []int{1,2,3,4,5} // [ pointer | 5 | 5 ]

newSlice := slice[1:3] // [ pointer | 2 | 4]

newSlice[2] = 45

// runtime error: index out of range [2] with length 2

append 切片後的長度在原陣列長度 -> 新切片指向原陣列

slice := []int{100,200,300,400,500}

newSlice := slice[1:3]

newSlice := append(newSlice, 600)

針對擴長後之新切片的操作會反映到原陣列

   slice := []int{100,200,300,400,500}
 [ 指標 | 5 | 5 ]
    |
   [0]     [1]     [2]     [3]     [4]
[  100  |  200  |  300  |  600  |  500  ] // 底層陣列
           | |
           | |
        ---- ----------------------------
        |                               |
     [ 指標 | 2 | 4 ]                 [ 指標 | 3 | 4 ]
       newSlice := slice[1:3]          newSlice := append(newSlice, 600)

append 切片後的長度超出原陣列長度 -> 新切片會指向複製原陣列之值的新陣列

append 函數會調節最大長度的擴長,針對長度 1000 以下的切片,會以兩倍增長,對於長度 1000 以上的切片則使用 1.25 的倍數做最大長度擴增。

slice := []int{100,200,300,400,500}

newSlice := append(newSlice, 600)

來看看記憶體的狀態:

   slice := []int{100,200,300,400,500}
 [ 指標 | 5 | 5 ]
    |
   [0]     [1]     [2]     [3]     [4]
[  100  |  200  |  300  |  400  |  500  ] // 底層陣列
            |
         [ 指標 | 2 | 4 ]
           newSlice := slice[1:3]

   newSlice := append(newSlice, 600)
 [ 指標 | 6 | 6 ]
    |
   [0]     [1]     [2]     [3]     [4]     [5]
[  100  |  200  |  300  |  400  |  500  |  600  |  0  |  0  |  0  |  0  ]
// 新的底層陣列

三參數指定切片

source := []int{100,200,300,400,500}
slice := source[2:3:4]

slice[0] // starting from source[2]
// Legth:    3 - 2 = 1
// Capacity: 4 - 2 = 2

erroredSlice := source[2:3:6]
// Runtime Error: panic: runtime error: slice bounds out of range

使用同樣的長度與最大長度的好處在於,當進一步透過 append 去拓展切片時,由於原 slice 的最大長度已用盡, append 會在底層新增一個陣列並且把值複製過去新的陣列,後續新增的值只會加到這個新陣列,不會對原陣列有變動。

range 迭代切片時,區塊內的值為新值

slice := []int{10, 20, 30, 40}

for index, value := range slice {
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
        value, &value, &slice[index])
}

// Output:
// Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
// Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
// Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
// Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

range 總是會從 0 開始迭代,如果有特殊的起始需求,可以使用一般的 for 操作:

slice := []int{1,2,3,4}

for index := 2; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

傳送切片為函數參數

由於切片所佔用的記憶體很少,和陣列不同,直接傳送一個大尺寸的切片進去函數裡並不會增加太多效能負擔。