Go語言學習8:深入理解切片slice

slice定義

slice是個結構體,源碼如下:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指針
    len   int // 長度 
    cap   int // 容量
}

slice 共有三個屬性:

  • 指針,指向底層數組;
  • 長度,表示切片可用元素的個數,也就是說使用下標對 slice 的元素進行訪問時,下標不能超過 slice 的長度;
  • 容量,底層數組的元素個數,容量 >= 長度。在底層數組不進行擴容的情況下,容量也是 slice 可以擴張的最大限度。

在這裏插入圖片描述

注意,底層數組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進行操作是有可能影響到其他 slice 的。

創建方法

創建 slice 的方式有以下幾種:

序號 方式 代碼示例
1 直接聲明var var slice []int
2 new slice := *new([]int)
3 字面量:= slice := []int{1,2,3,4,5}
4 make slice := make([]int, 5, 10)
5 從切片或數組“截取” slice := array[1:5]slice := sourceSlice[1:5]

不同方式創建出來的切片分爲 nil切片 和 空切片。

  • nil切片的長度和容量都爲0,和nil比較的結果爲true。
  • 空切片的長度和容量也都爲0,但是所有的空切片的數據指針都指向同一個地址 0xc42003bda0。空切片和 nil 比較的結果爲false。
創建方式 nil切片 空切片
方式一 var s1 []int var s2 = []int{}
方式二 var s4 = *new([]int) var s3 = make([]int, 0)
長度 0 0
容量 0 0
和 nil 比較 true false

截取

截取也是比較常見的一種創建 slice 的方法,可以從數組或者 slice 直接截取,當然需要指定起止索引位置。新 slice 和老 slice 或數組共用底層數組,新老 slice 對底層數組的更改都會影響到彼此。

值得注意的是,新老 slice 或者新 slice 老數組互相影響的前提是兩者共用底層數組,如果因爲執行 append 操作使得新 slice 底層數組擴容,移動到了新的位置,兩者就不會相互影響了。所以,問題的關鍵在於兩者是否會共用底層數組。

slice := data[2:4] // data[low, high]
slice := data[2:4:6] // data[low, high, max]

一般來說,截取後的第一個元素是low索引的元素,最後一個元素是high-1索引的元素。

最大容量則只能是索引 max-1 處的元素。high 和 max 必須在老數組或者老 slice 的容量(cap)範圍內。

當 high == low 時,新 slice 爲空slice

截取的例子

package main

import "fmt"

func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := slice[2:5]
    s2 := s1[2:6:7]

    s2 = append(s2, 100)
    s2 = append(s2, 200)

    s1[2] = 20

    fmt.Println(s1)
    fmt.Println(s2)
    fmt.Println(slice)
}

先看下代碼運行的結果:

[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]

我們來走一遍代碼,初始狀態如下:

slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]

s1 從 slice 索引2到索引4,長度爲3,容量默認到數組結尾,爲8。 s2 從 s1 的索引2到索引5,容量到索引6,爲5。
在這裏插入圖片描述
接着,向 s2 尾部追加一個元素 100:

s2 = append(s2, 100)

s2 容量剛好夠,直接追加。不過,這會修改原始數組對應位置的元素。這一改動,數組和 s1 都可以看得到。
在這裏插入圖片描述
再次向 s2 追加元素200:

s2 = append(s2, 100)

這時,s2 的容量不夠用,該擴容了。於是,s2 另起爐竈,將原來的元素複製新的位置,擴大自己的容量。並且爲了應對未來可能的 append 帶來的再一次擴容,s2 會在此次擴容的時候多留一些 buffer,將新的容量將擴大爲原始容量的2倍,也就是10了。
在這裏插入圖片描述
最後,修改 s1 索引爲2位置的元素:

s1[2] = 20

這次只會影響原始數組相應位置的元素。它影響不到 s2 了,人家已經遠走高飛了。
在這裏插入圖片描述
再提一點,打印 s1 的時候,只會打印出 s1 長度以內的元素。所以,只會打印出3個元素,雖然它的底層數組不止3個元素。

append

append 函數的參數長度可變,因此可以追加多個值到 slice 中,還可以用 ... 傳入 slice,直接追加一個切片。append函數返回值是一個新的slice。

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

使用 append 可以向 slice 追加元素,實際上是往底層數組添加元素。但是底層數組的長度是固定的,如果索引 len-1 所指向的元素已經是底層數組的最後一個元素,就沒法再添加了。

這時,slice 會遷移到新的內存位置,新底層數組的長度也會增加,這樣就可以放置新增的元素。同時,爲了應對未來可能再次發生的 append 操作,新的底層數組的長度,也就是新 slice 的容量是留了一定的 buffer 的。否則,每次添加元素的時候,都會發生遷移,成本太高。

其實 nil slice 或者 empty slice 都是可以通過調用 append 函數來獲得底層數組的擴容。


轉自:深度解密Go語言之Slice

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章