Go语言中的切片
1.切片的定义
Go语言中,切片是一个新的数据类型数据类型,与数组最大的区别在于,切片的类型中只有数据元素的类型,而没有长度:
md-end-block
var slice []string = []string{"a", "b", "c"}
因此,Go语言中的切片是一个可变长度的、同一类型元素集合,切片的长度可以随着元素数量的增长而增长,但不会随着元素数量的减少而减少,但切片底层依然使用数组来管理元素,可以看作是对数组做了一层简单的封装。
创建切片的方法共有三种,分别是基于数组、切片和直接创建。
1.1 基于数组创建切片
切片可以基于一个已存在的数组创建,切片可以只使用数组的一部分元素或者全部元素,甚至可以创建一个比数组更大的切片。
md-end-block
// 先定义一个数组
months := [...]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
// 基于数组创建切片
q2 := months[3:6] // 第二季度
summer := months[5:8] // 夏季
Go语言支持通过 array[start:end]这样的方式基于数组生成一个切片,start表示切片在数组中的下标七点,end表示切片在数组中的下表终点,两者之间的元素就是切片初始化后的元素集合,以下是几种创建切片的示例:
-
基于months 的所有元素创建切片(全年)
md-end-blockall := months[:]
-
基于 months 的前6个元素创建切片(上半年)
md-end-blockfirsthalf := months[:6]
-
基于第6个元素开始的后的后续元素创建切片(下半年)
md-end-blocksecondhalf := months[6:]
1.2 基于切片创建切片
类似于切片能够基于一个数组创建,切片也能够基于另一个切片创建:
md-end-block
firsthalf := months[:6]
q1 := firsthalf[:3] // 基于firsthalf的前三个元素构建新切片
基于切片创建切片时,选择的元素范围可以超过所包含元素的个数,如下:
md-end-block
// 基于切片创建切片
firsthalf := months[:6]
q1 := firsthalf[:3]
// 可以创建超过切片的元素
q3 := q1[:12]
如上图所示,q3长度远超过q1的长度,超出的部分由原数组months中的元素进行补充,那能不能超过这个原数组的长度呢?
产生了报错,显示切片的长度为13,但是容量是12,因此这里虽然是基于切片创建切片,但其本质依旧是基于数组创建切片。
1.3 直接创建切片
创建切片并不是一定需要一个数组,Go语言的内置函数make()可以灵活地创建切片。
创建一个初始长度位5的整型切片:
md-end-block
mySlice := make([]int, 5)
创建一个初始长度为5,容量为10的整型切片:
md-end-block
mySlice2 := make([]int, 5, 10)
创建并初始化包含5个元素的数组切片(长度和容量均为5):
md-end-block
// 这个语句容易和数组的初始化语句混淆
// 数组的初始化语句 array := [5]int{1,2,3,4,5}
// 这两个的区别在于切片初始化不需要指定切片长度,而数组需要指定数组长度
mySlice3 := []int{1, 2, 3, 4, 5}
和数组类型一样,所有未初始化的切片,会填充元素类型对应的零值。
实际上,使用直接方式创建切片时,Go底层还是会有一个匿名数组被创建出来,然后调用基于数组创建切片的方式返回切片,只是上层并不需要关心这个匿名数组的操作。因此,最终切片都是基于数组创建的,切片可以看作是操作数组的指针。
2 切片的遍历
前面提到,切片可以看作是数组指针,因此操作数组元素的所有方法也适用于切片,例如切片也能够使用下标获取元素,使用len()函数获取元素个数,并支持使用range关键字来快速遍历所有的元素。
传统的数组遍历方法:
md-end-block
for i := 0; i < len(summer); i++ {
fmt.Println("summer[", i, "] =", summer[i])
}
也可以使用range关键字遍历:
md-end-block
for i, v := range summer {
fmt.Println("summer[", i, "] =", v)
}
3 动态增加元素
切片与数组相比,优势在于支持动态增加元素,甚至能够在容量不足的情况,在切片类型中,元素个数和实际可分配的存储空间是两个不同的值,元素的个数即切片的实际长度,而可分配的存储空间就是切片的容量。
一个切片的容量初始值根据创建方式有以下两种情况:
-
对于基于数组和切片创建的切片而言,默认的容量是从切片起始索引到对应底层数组的结尾索引。
-
对于通过内置make函数创建的切片而言,在没有指定容量参数的情况下,默认容量和切片长度一致。
因此,通常情况下一个切片的长度值小于等于其容量值,能够通过Go语言内置的cap()函数和len()函数来获取某个切片的容量和实际长度:
md-end-block
var oldSlice = make([]int, 5, 10)
fmt.Println("len(oldSlice):", len(oldSlice))
fmt.Println("cap(oldSlice):", cap(oldSlice))
此时,切片 oldSilece 的默认值是 [0,0,0,0,0],可以通过append()函数向切片追加新元素:
md-end-block
newSlice := append(oldSlice, 1, 2, 3)
append() 函数的第二个参数是一个不定参数,可以根据自己的需求添加元素(大于等于1个),也可以直接将一个切片追加到另一个切片的末尾:
md-end-block
slice2 := []int{1, 2, 3, 4, 5}
// 注意append()后面的...不能省略
slice3 := append(newSlice, slice2...)
4 自动扩容
如果追加的元素个数超出切片的默认容量,则底层会自动进行扩容:
md-end-block
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := append(oldSlice, 6, 7, 8, 9)
fmt.Println("oldSlice:", oldSlice, "len:", len(oldSlice), "cap:", cap(oldSlice))
fmt.Println("newSlice:", newSlice, "len:", len(newSlice), "cap:", cap(newSlice))
此时,newSlice 的长度变成了9,容量变成了10,需要注意的是 append() 函数并不会改变原来的切片,而是会生成一个容量更大的切片,然后把原有的元素和新元素一并拷贝到新切片中。
默认情况下,扩容后的新切片容量将会是原切片容量的两倍,如果还不能够容纳新元素,则按照同样的操作继续扩容,直到新切片的容量不小于原长度与要追加的元素之和。但是,当原切片的长度大于或等于1024时,Go语言会以原容量的1.25倍作为新容量的基准。
在编码中,如果能够事先预估切片的容量并在初始化时合理地设置容量值,可以大幅降低切片内部重新分配内存和搬送内存块的操作次数,从而提升程序性能。
5 内容复制
Go语言提供了内置函数copy(),用于将元素从一个切片复制到另一个切片,如果两个切片不一样大,就会按照其中较小的那个切片元素个数进行复制。
md-end-block
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7, 8}
// 复制slice1到slice2,复制slice1的前三个元素到slice2中
copy(slice2, slice1)
fmt.Println("slice1:", slice1, "len:", len(slice1), "cap:", cap(slice1))
fmt.Println("slice2:", slice2, "len:", len(slice2), "cap:", cap(slice2))
slice3 := []int{1, 2, 3, 4, 5}
slice4 := []int{6, 7, 8}
fmt.Println("复制slice4到slice3")
// 复制slice4到slice3,复制slice4的所有元素到slice3的前三个元素
copy(slice3, slice4)
6 动态删除元素
切片除了支持动态增加元素之外,还可以动态删除元素,在切片中动态删除元素可以通过多种方式实现(底层是通过切片的切片实现):
md-end-block
slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice1 = slice1[:len(slice1)-5] // 删除 slice1 尾部 5 个元素
slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice2 = slice2[5:] // 删除 slice2头部 5 个元素
还能够通过 append 实现切片元素的删除:
md-end-block
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice4 := append(slice3[:0], slice3[3:]...) // 删除开头三个元素
注意append方法的使用, 如 slice4 := append(slice3[:0], slice3[3:]...)
这种方式:
-
slice3[:0]
创建了一个长度为 0 的切片,但底层数组仍然是slice3
的底层数组。 -
slice3[3:]
创建了一个包含slice3
从索引3开始的所有元素的切片。
append
将第一个切片的元素追加到第二个切片中,因此 slice4
包含 slice3
从索引3开始的所有元素。
这里的问题在于,由于slice4
最初共享底层数组,对 slice4
的修改实际上也会影响到 slice3
,从而导致 slice3
切片也发生了变化。
如果 slice4
由两个切片拼接,也会出现类似的问题,例如:
md-end-block
slice5 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice6 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice7 := append(slice5[:3], slice6[6:]...)
如果想要保证两个切片是完全独立的,不共享底层数组,可以使用copy函数来进行切片的删除。
使用 copy
函数进行元素的删除:
md-end-block
slice8 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice9 := make([]int, len(slice3)-3)
copy(slice9, slice3[3:]) // 删除开头前三个元素
7 数据共享问题
切片底层是基于数组实现的,对应的结构体对象如下所示:
md-end-block
type slice struct {
array unsafe.Pointer //指向存放数据的数组指针
len int //长度有多大
cap int //容量有多大
}
在结构体中使用指针存在不同实例的数据共享问题,示例代码如下:
md-end-block
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:3]
slice2[1] = 6
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
slice2
是基于 slice1
创建的,它们的数组指针指向了同一个数组,因此,修改 slice2
元素会同步到 slice1
,因为修改的是同一份内存数据,这就是切片的数据共享问题。
可以按照如下方式,避免切片的数据共享问题。
md-end-block
slice3 := make([]int, 4)
slice4 := slice3[1:3]
slice3 = append(slice3, 0)
slice3[1] = 2
slice4[1] = 6
fmt.Println("slice3:", slice3)
fmt.Println("slice4:", slice4)
虽然 slice2
是基于 slice1
创建的,但是修改 slice2
不会再同步到 slice1
,因为 append
函数会重新分配新的内存,然后将结果赋值给 slice1
,这样一来,slice2
会和老的 slice1
共享同一个底层数组内存,不再和新的 slice1
共享内存,也就不存在数据共享问题了。
如下代码,虽然使用了append函数,但是没有重新分配内存空间,仍然存在数据共享问题。
md-end-block
slice5 := make([]int, 4, 5)
slice6 := slice5[1:3]
slice5 = append(slice5, 0)
slice5[1] = 2
slice6[1] = 6
slice5
容量为5,执行 append
没有进行扩容操作。