slice 介绍

像其他语言一样,都是有数组,数组是具备固定类型和固定长度的,因此使用起来不是很方便,因此go引入了silce,表示一个拥有相同元素类型的可变长度的序列。
数组和slice是紧密关联的,slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素。slice中有三个属性:指针、长度和容量

1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
  • Data 是 指向数组的指针
  • 是当前切片的长度
  • Cap是当前切片的容量,即Data数组的大小

其中Data是一片连续的内存空间的数组
image.png

初始化

Go中的初始化包含三种方式

  1. 通过下标的方式获取数组或者切片的一部分
  2. 使用字面量初始化新的切片
  3. 使用关键字 make创建切片

    make([]T, length, capacity) 其中容量capacity是可选参数 也可以写成 make([]type, len)

1
2
3
arr[0:3] or slice[0:3]
slice := []int{1, 2, 3}
slice := make([]int, 10)

slice 中的坑

引起坑的原因

  • 函数是值传递,
  • slice结构中数据部分是 指向数组的指针
  • 打印slice受len控制
  • 如果发生扩容,会产生新的数组

直接通过一些案例来学习下

案例1

1
2
3
4
5
6
7
8
9
10
11
12
//情况一
func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
}

输出 1,2,3

分析

  1. go中函数是值传递
  2. 打印受len影响

image.png
此时slice的 len=3,cap=4,在执行TestSlice(slice)之后,外部slice传参到函数值,由于slice中持有的是数组的指针,且并没有发生扩容,所以这里两个slice指向的是同一个数组
image.png

后面打印的slice是 len为3,因此,打印结果是1,2,3,如果此时强行修改len=4,那么打印结果为1,2,3,4

1
2
3
4
5
6
7
8
9
10
11
func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
(*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 4 //强制修改slice长度
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
}

案例2

1
2
3
4
5
6
7
8
9
10
11
func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
slice[0] = 10
}

这个case跟案例一道理一样,并未出现扩容,执行TestSlice之后,两个slice数据部分仍然指向统一数据 10,2,3,4,打印slice跟len有关(len=3),因此

输出结果 10,2,3

案例3

1
2
3
4
5
6
7
8
9
10
11
12
//情况三
func main() {
slice := make([]int,0,3)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
slice[0] = 10
}

这个case的不同之处是涉及到了 扩容操作,导致数据部分指向了不同的数组

输出结果 1,2,3

slice 初始化时候 指定了容量为3,slice = append(slice,1,2,3) 之后 len=3,之后执行了TestSlice之后,len其实到了4,超过容量了,因此需要执行扩容操作

扩容逻辑:
1、根据策略申请一个更大的数组空间(slice容量的扩容规则:当原slice的cap小于1024时,新slice的cap变为原来的2倍;原slice的cap大于1024时,新slice变为原来的1.25倍)
2、copy 旧数组中的数据到新数组
3、添加新增的数据
4、将数组的指针复制给slice

image.png
扩容操作之后 通过 值传递进来的原slice 其实没有变化了,此时扩容之后两个slice数据部分分别指向不同的数组

slice正确使用

由于函数的参数是按值传递的,而 slice不仅保存了底层数组的引用,还保存了 slice所引用的范围。这就导致了上面的情况,底层数组改变了,而 slice没有改变。

  • 传递 slice的指针给函数
  • 将修改后的 slice返回并重新赋值
  • 最好的解决办法是不要在多个函数里修改 slice

在案例2中,在不扩容情况下 其实数据部分数组指针共用的,因此在函数内修改会影响到外部入参的slice,有时候我们希望在函数里修改slice但是又不希望影响外部数据怎么办?这时候就需要使用 cpoy函数复制要修改的slice到一个新的slice中去(这样底层数据数组就不相同了)
函数修改如下

1
2
3
4
5
6

func TestSlice(slice []int) {
newNums := make([]int, len(slice))
copy(newNums, slice)
newNums[0] = 10
}

参考