之前对切片的理解是本身是一个指针,指针指向数组,因为数组是值赋值,所以数组作为函数参数传递时,会被复制一份,传递较大数组时会比较浪费内存。
之前的理解部分对,但是瑕疵和错误的地方太多。
详解slice结构体
切片本身是一个结构体,它是被golang封装过的结构体,所以使用起来和数组差不多,其结构体内容如下:
type slice struct { array unsafe.Pointer // 指向数组的指针,核心所在 len int // 切片长度,在长度范围内,都可以像数组那样更新索引中的值,在len范围外会报越界错误 cap int // 容量,指针所指向的数组长度,必须大于等于len字段 } |
---|
在len字段长度范围的操作,和数组用法一致,例如:
var array [10]int slice:=make([]int,10,20) slice[9]=10 // 在索引值小于10时,都可以直接进行赋值 array[9]=10 |
---|
append函数和切片联系紧密,切片的赋值和append函数有关。
当需要给len字段长度之外的索引进行操作,需要使用到append函数,例如:
// fmt.Println(slice[10]) // 输出 panic: runtime error: index out of range,索引超出范围之外了 slice=append(slice,5,5) // 可以一次加入多个值(本次操作,添加了两个值),也可以直接追加一个切片 fmt.Println(slice[10]) // 输出为5 |
---|
进行append()操作,当len加上新加入的值个数大于cap时,则会触发扩容操作,即调用slice包的growslice进行扩容,扩容大致内容是:
len小于1000时,cap扩容两倍,创建一个2*cap长度的数组,将之前数组的内容赋值到新指针指向的数组,len>1000时,扩容因子为1.25倍。
此时新的slice和老的slice不是同一个结构体,指向的数组也不相同,append函数返回的是新slice,cap为扩容后的长度,新的len=老的len+新加入的值个数
数组和切片的区别
切片相当于对数组的一层封装,扩容方面比较灵活,数组初始化以后数组长度就再也不能变化了,想要扩容只能主动手写函数。
在当函数参数进行值传递方面,slice维护了数组的指针,加上本身的两个int字段,一共长24个字节,作为值传递时比较轻量级,数组值传递取决于数组的长度,如果是1e6长度的int数组,则每次传递需要重新开辟8m的内存创建一个新的数组,对空间浪费较大。
如果在函数传入的参数是数组指针,也只是需要占用8字节的空间,一个指针8字节(64位系统),这时和使用slice没有什么区别,只是扩容的灵活度方面不如slice。
建议
在我看来,使用slice有些需要注意的点,如果初始化了一个len为5,cap为10的切片,索引的前五位(0~4)都是可以直接赋值的,在前面的5位还没使用时,不要使用append函数添加新的值,这样会造成切片前面五个值的浪费,而且他们的内容都为零值,可能会和你的预期不一致。
在不能确定切片的起始长度时,可以初始化为make([]int,0,10)