slice定义
slice是个结构体,源码如下:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
slice 共有三个属性:
- 指针,指向底层数组;
- 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
- 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。
注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
创建方法
创建 slice 的方式有以下几种:
序号 | 方式 | 代码示例 |
---|---|---|
1 | 直接声明var | var slice []int |
2 | new | slice := *new([]int) |
3 | 字面量:= | slice := []int{1,2,3,4,5} |
4 | make | slice := make([]int, 5, 10) |
5 | 从切片或数组“截取” | slice := array[1:5] 或 slice := sourceSlice[1:5] |
不同方式创建出来的切片分为 nil切片 和 空切片。
- nil切片的长度和容量都为0,和nil比较的结果为true。
- 空切片的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false。
创建方式 | nil切片 | 空切片 |
---|---|---|
方式一 | var s1 []int | var s2 = []int{} |
方式二 | var s4 = *new([]int) | var s3 = make([]int, 0) |
长度 | 0 | 0 |
容量 | 0 | 0 |
和 nil 比较 | true | false |
截取
截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。新 slice 和老 slice 或数组共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。
值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append
操作使得新 slice 底层数组扩容
,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。
slice := data[2:4] // data[low, high]
slice := data[2:4:6] // data[low, high, max]
一般来说,截取后的第一个元素是low索引的元素,最后一个元素是high-1
索引的元素。
最大容量则只能是索引 max-1
处的元素。high 和 max 必须在老数组或者老 slice 的容量(cap)范围内。
当 high == low 时,新 slice 为空slice
。
截取的例子
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
先看下代码运行的结果:
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
我们来走一遍代码,初始状态如下:
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s1 从 slice 索引2到索引4,长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2到索引5,容量到索引6,为5。
接着,向 s2 尾部追加一个元素 100:
s2 = append(s2, 100)
s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。
再次向 s2 追加元素200:
s2 = append(s2, 100)
这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。
最后,修改 s1 索引为2位置的元素:
s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。
再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。
append
append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ...
传入 slice,直接追加一个切片。append函数返回值是一个新的slice。
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。
这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。
其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。