前言:切片的设计思想来源于动态数组,是为了开发者能更加方便地使用使一个数据结构能自动增加和减少,但是切片本身并不是动态数组。
1、切片与数组
Go是值传递的,用切片传递数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。但是也有反例,并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗未必比make消耗大。
2、切片的数据结构
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写限定在制定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
type slice struct {
array unsafe.Pointer
len int
cap int
}
3、创建切片
创建切片有两种形式,make 创建切片,空切片。
3.1、make 和切片字面量
3.2、nil和空切片
var slice []int
空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。
silce:=make([]int,0)
slice:=[]int{}
空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
4、切片扩容
Go 中切片扩容的策略是这样的:
1)首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
2)否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
3)否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
4)如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
新数组 or 老数组 ?
由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。这种情况也极容易出现在字面量创建切片时候,第三个参数 cap 传值的时候,如果用字面量创建切片,cap 并不等于指向数组的总容量,那么这种情况就会发生。
情况二:
情况二其实就是在扩容策略里面举的例子,在那个例子中之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。
所以建议尽量避免情况一,尽量使用情况二,避免 bug 产生。
4、切片迭代 for range
由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。