数组和切片
数组(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的时候推导了数组的长度,长度也是数组类型的一部分,用上面的方法也可以把长度推导出来。
有疑问加站长微信联系(非本文作者)