1. 程式人生 > >Go36-7-數組和切片

Go36-7-數組和切片

print main 對數 clas 調用 增長 字典 例子 估算

數組和切片

數組(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的時候推導了數組的長度,長度也是數組類型的一部分,用上面的方法也可以把長度推導出來。

Go36-7-數組和切片