切片是go中非常重要的一种基础结构,一个切片由3部分组成:指针、长度和容量。指针指向底层数组,长度代表slice当前的长度,容量代表底层数组的长度
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 长度
cap int // 容量
}
本文将介绍切片的一些最佳实践,以及切片的常用操作append,截取,复制在底层是如何实现的
切片的初始化方式有好几种,分别为:
var s []int
此时切片s的cap = 0,len = 0,data = nil,虽然data等于nil,但不影响往s append元素,因此和
s := []int{}
初始化方式在使用上没有区别
s := []int{1,2,3}
该切片cap = len = 3,data为指向底层数组的指针,该数组容量为3,装有3个元素
s := make([]int,3,5)
切片s的len=3,cap=5,data指向一个底层数组,该数组容量为5,其中每个值都是默认值
建议使用make初始化方式,因为只有make能指定容量,当预先知道数据规模时,可以预分配合适的内存空间,避免后续使用时频繁扩容
需要注意用make初始化时,除了copy切片时(下文介绍),len需要为0,表示当前slice有0个元素,append时从0位置开始追加。若len不为0,就会从len位置开始追加,len前面的位置就用不到了
slice作为动态数组,长度是可以变化的,实现方式就是使用append
如果追加完后len小于等于cap,就会在当前使用的底层数组后面追加元素
如果追加完后len大于cap了,就会进行扩容。注意这里是大于slice的cap,而不是底层数组的容量,虽然这两者大部分情况是相等的,也就是说可能出现一种情况是,切片中元素数量还没到底层数组的容量,就会进行扩容。这种情况在切片截取时会发生,我们后面再讨论
func main() {
s := make([]int, 0, 3)
fmt.Println(s, len(s), cap(s))
s = append(s, 1)
fmt.Println(s, len(s), cap(s))
}
// 执行结果
[] 0 3
[1] 1 3
需要注意append之后,一定要用原来的变量s去接收
因为原来的变量s的len = 0,没有变化,对原来的变量s进行遍历,虽然底层数组已经有1个元素,但slice能访问到哪些元素,是根据len决定的,所以看起来还是空切片
append返回的slice变量中len = 1,cap = 3,此时用s接收该返回值,s的len变为1,就能访问到刚append的元素
继续对切片执行append,直到len超过cap,就会触发扩容,新申请一块底层数据,将原数组的数据拷贝过去
func main() {
s := make([]int, 0, 3)
fmt.Println(s, len(s), cap(s))
s = append(s, 1)
fmt.Println(s, len(s), cap(s))
s = append(s, 2, 3, 4)
fmt.Println(s, len(s), cap(s))
}
// 执行结果
[] 0 3
[1] 1 3
[1 2 3 4] 4 6
关于新数组的容量,在runtime/slice.go.growslice中有说明:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
若2倍的老容量oldcap还小于需要的容量,就直接用需要的容量
当oldcap小于1024时,使用2 * oldcap作为新数组的容量
否则新容量 = oldcap * 1.25
多分配一些空间,避免每次追加都要重新申请内存,但长度超过1024时只多分配 25%的空间,避免浪费
除此之外,还会进行内存对齐操作,因为go内存分配是按照size class大小进行分配,需要根据需求内存大小,在runtime/sizeclasses.class_to_size
数组中,找到最小的满足需求大小的一块内存,给slice使用
该数组定义如下:
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
例如我需要能装下5个int数据,也就是40字节的空间,就会找到大小为class_to_sizep[4] = 48
字节的空间进行分配
这个道理是,这段剩余的内存就算不给该切片,也不会给别人使用了,因为就给该切片,减少内部碎片
切片支持以下截取操作:
func main() {
s := []int{1, 2, 3, 4, 5, 6}
s1 := s[1:3]
fmt.Println(s1, len(s1), cap(s1))
}
// 结果
[2 3] 2 5
s1从s中,根据开头和结尾进行截取出[1,3)范围的数,截取出2个元素,因此len就是2,cap的值为此次截取的开头算到原切片的cap结尾,也就是5,data还是为为s的data
可以看出,截取操作代价很低,就是new一个slice结构,底层数组复用原来的,通过slice结构的len和cap两个字段,决定当前slice能访问哪些元素,数据量到多少时需要扩容
此时如果对往s1后面append,因为len还没超过cap,就会在data指向的数组后面追加,且该数组和s为同一个,就会修改原数组下标为3位置的值:
func main() {
s := []int{1, 2, 3, 4, 5, 6}
s1 := s[1:3]
fmt.Println(s1, len(s1), cap(s1))
s1 = append(s1, 8)
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s, len(s), cap(s))
}
//
[2 3] 2 5
[2 3 8] 3 5
[1 2 3 8 5 6] 6 6
可以看到,s和s1的数据都被改变
切片也支持指定最大cap的截取操作:
func main() {
s := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s, len(s), cap(s))
s2 := s[1:3:3]
fmt.Println(s2, len(s2), cap(s2))
}
// 结果
[1 2 3 4 5 6] 6 6
[2 3] 2 2
s2从s中也开头和结尾进行截取出[1,3)范围的数,但加了最大值限制3,s2的cap就从此次截取的开头1算到最大值限制3,也就是2
此时如果对s2执行append,就会命中上文的情况:明明切片中元素数量还没到底层数组的容量,但len达到了cap的值,就会进行扩容:
func main() {
s := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s, len(s), cap(s))
s2 := s[1:3:3]
fmt.Println(s2, len(s2), cap(s2))
s2 = append(s2, 7)
fmt.Println(s2, len(s2), cap(s2))
}
//
[1 2 3 4 5 6] 6 6
[2 3] 2 2
[2 3 7] 3 4
可以看到,s[3]位置的值还是4,没有被s2影响,说明此时s和s2底层使用的是两个数组
go内建函数copy能完成切片的拷贝:
func main() {
s := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s, len(s), cap(s))
sc := make([]int, 6, 6)
copy(sc, s)
fmt.Println(sc, len(sc), cap(sc))
}
// 结果
[1 2 3 4 5 6] 6 6
[1 2 3 4 5 6] 6 6
copy函数将原切片s的内容拷贝的目标切片sc。要拷贝多少个元素,是根据min(len(s),len(sc))决定的,因此需要将目标切片的len设置为和原切片一样,才能保证将原切片原封不动地拷贝到目标切片
可以通过for循环来遍历切片,其中i代表下标,v代表切片中的每个元素
s := []int{1,2,3,4,5}
for i,v := range s {
// do something
}
这里有个坑,若想修改切片中的元素,若果修改for的循环遍历v,例如下面想将每个元素 * 2,会发现不生效:
s := []int{1,2,3,4,5}
for _,v := range s {
v *= 2
}
这是因为这里操作的其实是一个临时变量,而不是切片中的值,具体原理可以参考详解go语言中for range的坑 - 掘金
要想修改切片中的元素,需要拿到下标直接操作切片本身:
s := []int{1,2,3,4,5}
for i,v := range s {
s[i] *= 2
}