前言
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。 本文将深入切片的本质,并讲解它的用法。
切片
数组虽然有适用它们的地方,但是数组不够灵活,因此在Go代码中数组使用的并不多。 但是,切片则使用得相当广泛。切片基于数组构建,但是提供更强的功能和便利。
切片类型的写法是 []T , T 是切片元素的类型。和数组不同的是,切片类型并没有给定固定的长度。
letters := []string{"a", "b", "c", "d"}
我们可以创建一个var s []byte
然后往切片里边放入值,
s[0] = 0
s[1] = 1
s[2] = 2
s[3] = 3
s[4] = 4
或者也可以这样:
s = append(s,0)
s = append(s,1)
s = append(s,2)
s = append(s,3)
s = append(s,4)
或者索性这样:
s = append(s,0,1,2,3,4)
总之切片是这样的
var s[]int = []int{0,1,2,3,4}
切片也可以使用内置函数 make 创建,函数签名为:
s := make([]byte, 5)
可以使用内置函数 len 和 cap 获取切片的长度和容量信息。
len(s) == 5
cap(s) == 5
切片的底层原理
一个切片是一个数组片段的描述(实际上是个整形数组)。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。如下图
前面使用 make([]byte, 5) 创建的切片变量 s 的结构如下:
长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。我们可以看到 长度len = 5 , 容量cap = 5。 因为我们使用make([]byte, 5)并没有指定cap的大小,系统默认cap = 5,其使用效果与make([]byte, 5,5)一样。
为了更一步了解长度和容量的区域,我们继续对 s 进行切片,观察切片的数据结构和它引用的底层数组:
S1 = s[2:4]
通过 s1=s[2:4] 操作后,我们发现s1的切片的len=2,cap=3。 s1的指针指向了s数组索引为2的数组位置,s1切片所指向的数据为{2,3} 这也是左闭右开原则,所以长度len=2, 但因为指针指向s数组索引位置2,其当前切片的可用的容量是从当前指针位置往后数,即当前指针到最后有3个位置空间。即cap=3。 这一切的一切只说明了s1,但s数组的len和cap并没有因此改变。
接着
s1 = append(s,8)
S1追加了一个数据,那么追加的这个数据’8’放到了s1指针切片的末端也就是s数组的索引为4的位置,此时追加改变了 s1的len(len=3) 同时也改变了s数组索引4位置的数据。故修改了原先数组!
好了我们已经对切片有了简单的认识了,接下来我们深入以下切片特性,请明白:
1: 占用资源很小
2: slice[a:b] 包含a索引值,不包含b索引值,默认容量上界索引为被操作对象容量上界索引
3: slice[a:b:c] 包含a索引值,不包含b索引值,容量上界索引为c(cap中不包含索引C位置)
4: slice 只能向后扩展,不能向前扩展
5: slice append
(1)某次操作未超过该slice容量上界索引,此次改变会更新原数组;
(2)某次操作超过该slice容量上界索引则新的被操作对象数组会被新建,此次改变不会更新原数组
前面创建的切片 s1 追加后长度依旧小于s1的容量。如果追加的长度超过它本身容量的话该如何扩容呢?
这次我们把s数组重新初始化,10个数组元素分别是0,1,2,3,4,5,6,7,8,9
var s []int = [...]int{0,1,2,4,5,6,7,8,9}
var s0 [] int
s0 = s[3:9]
s0的len=6 , cap = 7 (ps:为什么cap = 7,因为其可用的容量是从当前指针位置往后数到最大上限,这里容量的上限没有给定,故索引因此默认为数组的容量上限索引为10)
接着我们定义s1切片并为s1扩容
s1 := s[3:6:8]
图中已经标出属于s1所指的区域和它的容量区域 , 接着进行追加数据如下
s1 = append(s1, 100)
我们发现跟预期一样,没有超出容量范围追加数据直接在原先len后追加,s1所指数据由3,4,5,100。同时这个过程中改变了原先s数组的索引为6的数据,改为100。
我们再连续继续追加两个数据:
s1 = append(s1, 100, 100)
其实s1的容量已经存放不下了,此次 超过该s1容量上界索引”8”, 不更新原数组, 新建数组。新建尺寸的大小是cap的大小。(划重点!切片扩容都是以cap为尺寸大小扩容,内存大小是cap的整数倍)
如图所示,此时,s1的len=6, cap=10 。s1={3,4,5,100,100,100}
s={0,1,2,3,4,5,100,7,8,9}
代码
// 切片
// 1: 占用资源很小
// 2: slice[a:b] 包含a索引值,不包含b索引值,默认容量上界索引为被操作对象容量上界索引
// 3: slice[a:b:c] 包含a索引值,不包含b索引值,容量上界索引为c
// 4: slice 只能向后扩展,不能向前扩展
// 5: slice append
// (1)某次操作未超过该slice容量上界索引,此次改变会更新原数组;
// (2)某次操作超过该slice容量上界索引则新的被操作对象数组会被新建,此次改变不会更新原数组
numArr := [10]uint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice0 := numArr[3:9] // 容量上界索引为10
slice1 := numArr[3:6:8] // 容量上界索引为8
slice1 = append(slice1, 100) // 此次 未超过该slice容量上界索引 更新原数组, 不新建数组
slice1 = append(slice1, 100, 100) // 此次 超过该slice容量上界索引 不更新原数组, 新建数组
var s []int = []int{1, 2, 3, 4}
fmt.Print(s)
for i := 0; i < len(slice0); i++ {
fmt.Println(i, slice0[i])
}
fmt.Println("slice0 ==>", len(slice0), cap(slice0))
for i := 0; i < len(slice1); i++ {
fmt.Println(i, slice1[i])
}
fmt.Println("slice1 ==>", len(slice1), cap(slice1))
for i := 0; i < len(numArr); i++ {
fmt.Println(i, numArr[i])
}
fmt.Println("slice ==>", len(numArr), cap(numArr))