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倍。
驗證一下上面的擴容策略:
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-數組和切片