切片的定义
切片(slice)又称动态数组,依托数组实现,可以方便的进行扩容、传递等,实际使用中比数组更灵活。
底层数据结构
切片的底层数据结构是一个结构体,其中包含 指向底层数组的指针、切片的长度、切片的容量 三部分。切片的数值都存储在底层数组中。
长度和容量有什么区别呢?
长度指的是切片能操作到的底层数组的一部分,容量指的是底层数组的长度。
当向切片中添加的元素个数超出了切片的长度,超出的元素时可以不必重新分配内存,直接使用预留内存(即剩余容量)即可。
看不懂没关系,下文有例子可以帮助理解。
常见功能
使用make创建切片
make函数用于初始化一个切片,可以同时指定长度和容量,也可以只制定长度,不指定容量(此时容量默认和长度值相等)。
创建时底层会分配一个数组,数组的长度即容量。
例如,语句slice := make([]int, 5, 10) // 其中5为长度,10为容量。
表示创建了一个长度为5,容量为10的 int 切片。
切片长度为5,表示可以使用下标slice[0] ~ slice[4]来操作切片里面的元素。若超出该下标则会报错,即使该下标在容量范围内也不行。例如slice[8]就会报错。
切片容量为10,表示后续向切片添加新的元素时可以不必重新分配内存,直接使用预留内存即可。
使用append向切片中添加元素
append函数用于向切片中添加元素。
例如: append(sli, 1, 2, 3) // 添加的元素个数不固定
表示向切片sli中添加了1、2、3,共三个元素。
添加元素的具体步骤:
使用append()向切片添加一个元素的实现步骤如下:
先判断切片容量是否够用,
若切片容量够用,则将新元素追加进去,然后切片长度增加,返回切片;
若切片容量不够,则将切片先扩容,扩容后切片会指向一个新的且长度更长的底层数组,此时新切片的长度没变,容量变为了新数组的长度。接着将新元素追加进新切片,新切片的长度增加,返回新的切片;
难么,新底层数组的长度是多少呢?
切片的扩容机制:
我们向切片中添加元素,长度会不断增加,当长度大于容量时,切片会发生扩容——切片会指向新开辟的数组,原数组中的元素会自动追加到新数组中。切片的容量也会增大。这样就相当于得到一个新切片。
切片的扩容机制历经过2个版本的变化,我们讲一下当前最新版的1.22。
Go 1.22 引入了 nextslicecap 函数来计算新容量,取代了旧版本的直接翻倍或固定比例扩容策略。具体规则如下:
若期望容量超过当前容量的两倍:直接使用期望容量作为新容量。
若当前容量小于阈值(默认 256):容量翻倍(即 newcap = oldcap * 2)。
若当前容量 ≥ 阈值:采用渐进式扩容,每次增加 (当前容量 + 3*阈值) / 4,直到满足需求。这种策略使扩容系数从 2 倍平滑过渡到约 1.25 倍,减少大切片的内存浪费。
示例:
旧容量为 512 时,新容量计算为 512 + (512 + 3*256)/4 = 512 + 320 = 832,扩容系数约 1.625 倍。
随着容量增大,扩容系数逐渐趋近 1.25 倍。
了解这些之后,我们做一道题来巩固一下。
题目如下:
心中有结果了吗?往下看答案吧
首先创建了一个切片,长度为2,故此时底层数组为 [0, 0]。
第一次添加,先将元素添加到底层数组为[0, 0, 1],接着长度增加为3。
第二次添加,因切片长度大于容量,先触发扩容,使切片指向新底层数组,接着将元素添加到新底层数组[0, 0, 1, 2], 之后新切片长度增加为4。
新切片的容量为6,是原切片容量的2倍。
copy
copy()函数用于将一个切片的数据逐个拷贝到另一个切片中,拷贝元素数量为两个切片长度的最小值(例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。)。
例如: copy(sli2, sli) // 将sli的元素拷贝到sli2中
示例:
截取数组或切片生成新切片
我们可以通过对数组或切片进行截取,从而获得到一个新切片。
新切片与原切片共用一个底层数组。
例如 slice := array[start: end]
新切片slice的长度是end-start,容量是从start开始直至array的结束。
示例:
slice := array[:end]
表示从array的索引位置0到end处所获得的切片,len=end
slice := array[start:]
从array的索引位置start到len(array)-1处所获得的切片
还有另一种不常见的写法:
slice[start: end: max]
这样写是为了限制新切片的容量, 其中max-start表示新切片的容量,end-start表示新切片的长度。
0<=start<=end<=max<=cap(原切片)
示例:
让我们做一道题巩固一下:思考下方打印结果
答案:
作为函数参数
切片可以作为函数的参数,在函数内部可以对切片进行操作。但有一点需要注意:
go语言中,函数传参都是传递一个拷贝的副本。
有的副本是非引用类型,那么就无法在函数内部对参数的原值进行操作
有的副本是引用类型,那么就可以通过指针指向原值的地址,在函数内部实现对原值的操作。
切片的本质上是一个结构体,其中的长度、容量为非引用类型,但指向底层数组的指针为引用类型。
所以,函数内部可以对切片参数进行底层数组上的操作,但无法操作其原本的长度和容量。
懂了这些之后,我们上两道题实践一下。
思考一下,会输出什么内容?
package main
import "fmt"
func sliceRise(s []int) {
s = append(s, 0)
for i := range s {
s[i]++
}
}
func main() {
s1 := []int{1, 2}
s2 := s1
s2 = append(s2, 3)
sliceRise(s1)
sliceRise(s2)
fmt.Println(s1, s2)
}
结果:
先看s1,在传入函数之前,s1的长度为2,容量为2。传入函数之后,函数内对s1的副本append了一个元素,这时s1副本的长度大于容量,发生扩容,使得s1副本指向了一个新的底层数组,之后函数内部对s1副本的自增都是发生在新的底层数组,所以最后main函数中打印的s1的值不变。
再看s2,在传入函数之前,s2由s1直接赋值得到,长度为2,容量为2,对s2进行append之后,s2发生扩容,长度变为3,容量变为4,指向新的底层数组。传入函数之后,函数内对s2副本append一个元素,s2副本长度变为4,此时s2副本容量也为4,未发生扩容,所以未指向新的底层数组,所以之后对s2副本进行的自增都是发生在指向的原底层数组上,所以最后在main函数中打印s2的值会发生自增。函数内部s2副本的长度增加到4,但增加的只是副本的长度,非引用类型的副本变化不改变原值,所以在main函数中的s2的长度是不变的,依旧为3。所以最后结果为 [2, 3, 4]。
再看一题:
思考一下,会输出什么内容?
func main() {
x := make([]int,0,10)
x = append(x,1,2,3)
y := append(x,4)
z := append(x,5)
fmt.Println(x)
fmt.Println(y)
fmt.Println(z)
}
答案如下:
先说x,最开始,x由make函数创建而来,长度为0,容量为10,对x进行append之后,x的长度变为3,容量为10。
接着,y:=append(x, 4),表示创建一个新切片,新切片的底层数组为将元素4添加到x底层数组之后的底层数组,新切片的长度为x的长度增加之后的长度,接着将新切片赋值给y。也就是说,x的底层数组变了,为[1, 2, 3, 4, 0, 0, 0, 0, 0 ,0],但x的长度是不变的,仍为3。y与x共用一个底层数组。
接着,z:=append(x,5),向x中添加元素5,此时x长度为3,添加的5只能占用x的第4个索引位置,也就是要覆盖元素4,所以x底层数组变为[1, 2, 3, 5, 0 ,0 ,0 ,0 ,0 ,0]。z与x也共用一个底层数组。
所以最后输出的结果y与z是相同的,他们俩都共用x的底层数组,他们俩的长度都为4。