1. 程式人生 > >Go36-7-陣列和切片

Go36-7-陣列和切片

陣列和切片

陣列(array)型別和切片(slice)型別:
相同:都屬於集合類的型別,它們的值都可以用來儲存某一種型別的值(或者說元素)。
不同:陣列的長度是固定的,而切片是可變長的。

長度
陣列的長度在宣告的時候必須確定,並且之後不會再變。長度是其型別的一部分。
比如:[1]string 和 [2]string 是兩個不同的型別。
切片的長度是可以隨著其中元素的增長而增長的,但是不會隨著元素的減少而減少。

底層陣列
可以把切片看做是對陣列的一層簡單的封裝,每個切片的底層資料結構中,一定會包含一個數組。這個陣列可以被叫做切片的底層陣列。而切片可以被看做是對陣列的某個連續片段的引用。

值型別、引用型別

切片屬於引用型別,陣列屬於值型別
引用型別:

  • 切片
  • 字典
  • 管道
  • 函式

值型別:

  • 陣列
  • 結構體

長度和容量

陣列和切片都用長度和容量。呼叫len函式,可以得到長度,呼叫cap函式可以得到容量。
陣列的容量永遠等於長度,並且是不可變的。
關於切片的容量和長度,看下面的例子:

package main

import "fmt"

func main() {
    s1 := make([]int, 5)
    fmt.Println(len(s1), cap(s1), s1)
    s2 := make([]int, 5, 8)
    fmt.Println(len(s2), cap(s2), s2)
}

先用make聲明瞭一個[]int型別的變數s1,並且傳遞了一個第二個引數5。指明瞭切片的長度。
用同樣了方式聲明瞭切片s2,這次多傳遞了一個引數8,指明瞭切片的容量。
s1的長度是5,通過宣告指定。容量也是5,宣告時沒有指定容量,就和長度一致。
s2的長度是5,通過宣告指定。容量是8,也是通過宣告進行指定。
下面的切片表示式,可以把s2擴充套件到其當前最大容量:

s2[0:cap(s2)]

擴容

當切片無法容納更多的元素時,Go語言就會對切片進行擴容。擴容不會改變原來的切片,而是會生成一個容量更大的切片,然後把原有的元素和新元素拷貝到新切片中。
一般情況下,擴容會把新切片的容量(新容量)變成原來切片容量(原容量)的2倍。
當切片的長度大於或等於1024是,擴容是以1.25倍來增加的。
驗證一下上面的擴容策略:

package main

import "fmt"

func main() {
    s1 := make([]int, 3)
    fmt.Println(len(s1), cap(s1), s1)  // 當前容量3
    s1 = append(s1, 1)
    fmt.Println(len(s1), cap(s1), s1)  // 容量翻倍,變成6
    s2 := make([]int, 1023)
    fmt.Println(len(s2), cap(s2))  // 當前容量1023
    s2 = append(s2, 1)
    fmt.Println(len(s2), cap(s2))  // 看著像翻倍,不過是1024的翻倍2048
    s3 := make([]int, 1024)
    s3 = append(s3, 1)
    fmt.Println(len(s3), cap(s3))  // 容量變為1.25倍
}
/* 執行結果
PS G:\Steed\Documents\Go\src\Go36\article07\example02> go run main.go
3 3 [0 0 0]
4 6 [0 0 0 1]
1023 1023
1024 2048
1025 1280
PS G:\Steed\Documents\Go\src\Go36\article07\example02>
*/

驗證下來,似乎有一點小偏差。
另外,如果一次追加的元素過多,按上面的規則做一次擴容不夠,最終還是會擴容到一個比需要的容量大一些或者正好的容量,不過具體情況有點複雜。可以自己試試看,下面的例子一次可以往切片裡追加大量的元素:

package main

import "fmt"

func main() {
    s1 := make([]int, 5, 8)
    var s []int
    s = append(s1, make([]int, 88-5)...)
    fmt.Println(len(s), cap(s))
    s2 := make([]int, 1024)
    s = append(s2, make([]int, 2048-1024)...)
    fmt.Println(len(s), cap(s))
}

不必太在意切片“擴容”策略中的一些細節,只要能夠理解它的基本規律,並可以進行近似的估算就可以了。
不過如果有興趣,更多細節可參見runtime包中slice.go檔案里的growslice及相關函式的具體實現。

替換底層陣列

無需擴容時,append函式返回的是指向原底層陣列的新切片。其實就是底層陣列還在那,切片的下標往後加了幾位,可以指向到底層陣列後面更多的元素了。
需要擴容時,append函式返回的是指向新底層陣列的新切片。會建立一個更大容量的新的底層陣列,將元素從原切片複製到新的底層陣列,然後追加新元素。
希望下面的例子可以說明這裡的問題:

package main

import "fmt"

func main() {
    a := [...]int{1,2,3,4,5}  // 這是一個數組,將作為下面切片的底層陣列
    s1 := a[:3]
    s1 = append(s1, 11)  // 未發生擴容
    fmt.Println(s1, a)  // s1的新底層陣列還是a,往s1末尾新增元素,將覆蓋a裡原來的值
    s2 := a[:3]
    s2 = append(s2, 21, 22, 23, 24, 25)  // 容量不夠,需要擴容
    fmt.Print(s2, a)  // 現在a不再是s2的底層陣列了,這裡會複製一份陣列到一個新的陣列,作為新的底層陣列。a裡的元素不會被覆蓋
}

這裡生成陣列a的時候推導了陣列的長度,長度也是陣列型別的一部分,用上面的方法也可以把長度推匯出來。