go 语言的 slice 注意点
前言
起因是在编写某个模块时, 考虑到slice
良好的伸缩性, 便使用slice
来存储大量的数据, 业务是slice
的数据会慢慢的弹出减少, 理想状态下内存占用会越来越少, 结果发现, 占用的内存并不会返还, 于是查了一下, 好家伙 😵💫, 坑还不少, 索性这里记录一下坑吧
slice
slice
其实是一个指向底层数组的指针结构体, 这个结构体有三个属性, 分别是: 指向数组的指针
- 元素的长度
- 底层数组的长度
我们都知道, 数组是固定长度的, 而切片是动态扩容的, 所以, 切片结构有一个底层数组的长度, 使用cap()
可以查看, 当发现插入后的长度大于原先的底层数组的长度时, 会进行扩容, 即将申请一个新的数组,将原来的数据放置到新的数组中, 将指向数组的指针也修改为新的指针
slice
扩容的策略是:
- 原先的数组长度 < 1024 时, 扩容后的容量为原先的2倍
- 原先的数组长度 > 1024 时, 扩容后的容量为原先的1.25倍
数据共享问题
看以下代码
func t1() {
a1 := []int{1, 2, 3, 4}
a2 := a1
a3 := a1[:3]
a1[0] = 5
fmt.Println(a1, a2, a3)
}
运行结果是
[5 2 3 4] [5 2 3 4] [5 2 3]
可以看到, 只修改了a1
, 却造成了a2
和a3
一起改变, 这代表此时a1/a2/a3
共享了数据
我们再看代码
func t2() {
a1 := []int{1, 2, 3, 4}
a2 := append(a1, 6)
a3 := a1[:3]
a1[0] = 5
fmt.Println(a1, a2, a3)
}
结果是
[5 2 3 4] [1 2 3 4 6] [5 2 3]
这里的a1/a3
共享了数据, a2
就单独使用了, 这是因为append()
会面临内存的重新分配问题, 在a2
进行append()
时候, 重新申请了内存空间, 将a1
指向的数据拷贝一份, 然后增加了新值5
, 放入了新的地址中, 此时a2
的地址与a1/a3
不一样, 所以对a1
进行修改只影响到了a1/a2
再看代码
func t3() {
a1 := []int{1, 2, 3, 4}
a2 := append(a1, 6)
a3 := a1[:1]
fmt.Println(cap(a3))
a3 = append(a3, 6)
fmt.Println(cap(a3))
a1[0] = 5
fmt.Println(a1, a2, a3)
}
运行结果
4
4
[5 6 3 4] [1 2 3 4 6] [5 6]
我们这里发现了两个疑点
a3
经过append()
还是与a1
共享数据
还记得最开始提到的切片扩容机制吗? 没错, 对于append()
, 在执行时也会有两种分支区别, 只有容量不够才会进行重新分配, 步骤与普通的切片扩容一致
如果容量够, 比如这里, a3
的容量与a1
一致是4
, 而a3
长度由1
增加到2
, 没有超过4
, 因此没有进行内存的重新分配, 导致a3/a1
继续共享数据
而a2
在append()
时, 长度变成5
, 原有的容量不够, 因此进行了内存的重新分配
对a3
进行append()
在a1
的中间进行了数值插入
因为a3
是截取的a1[:1]
, 所以对a3
进行append()
在不重新分配内存的时候就会在a[0]
后添加数据
变量逃逸问题
因为存在变量共享的问题, 那么变量逃逸也就出现了
举个例子
func t5() {
a1 := []int{1, 2, 3, 4, 5, 6}
b1 := a1[:2]
// a1 不再使用
fmt.Println(b1)
}
假如a1
很大, 占用很多内存, 此时你只想获取a1
的某一段数据, 而a1
以后不再使用, 因为变量共享, 此时就会导致 GC 时a1
的长切片不会被回收, 仍然存在与内存中, 如果这个函数一直运行, 这将是一个很大的隐患
正确做法是, 在明确a1
不使用时, 将a1
赋值成nil
, 例如
func t5() {
a1 := []int{1, 2, 3, 4, 5, 6}
b1 := []int{} // 创建一个空切片
b1 = append(b1, a1[:2]...) // 通过 append 添加需要的元素
a1 = nil // 将 a1 设置为 nil
// a1 不再使用
fmt.Println(b1, cap(b1))
}
运行结果为
[1 2] 2