我们先看看切片的结构定义,reflect.SliceHeader
:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
可以看出切片的开头部分和Go字符串是一样的,但是切片多了一个Cap
成员表示切片指向的内存空间的最大容量(对应元素的个数,而不是字节数)。下图是x := []int{2,3,5,7,11}
和y := x[1:3]
两个切片对应的内存结构。
让我们看看切片有哪些定义方式:
var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3
d = c[:2] // 有2个元素的切片, len为2, cap为3
e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3
f = c[:0] // 有0个元素的切片, len为0, cap为3
g = make([]int, 3) // 有3个元素的切片, len和cap都为3
h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)
和数组一样,内置的len
函数返回切片中有效元素的长度,内置的cap
函数返回切片容量大小,容量必须大于或等于切片的长度。也可以通过reflect.SliceHeader
结构访问切片的信息(只是为了说明切片的结构,并不是推荐的做法)。切片可以和nil
进行比较,只有当切片底层数据指针为空时切片本身为nil
,这时候切片的长度和容量信息将是无效的。如果有切片的底层数据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了(比如直接通过reflect.SliceHeader
或unsafe
包对切片作了不正确的修改)。
遍历切片的方式和遍历数组的方式类似:
for i := range a {
fmt.Printf("a[%d]: %d
", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d
", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d
", i, c[i])
}
其实除了遍历之外,只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader
),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。
如前所说,切片是一种简化版的动态数组,这是切片类型的灵魂。除了构造切片和遍历切片之外,添加切片元素、删除切片元素都是切片处理中经常遇到的问题。
添加切片元素
内置的泛型函数append
可以在切片的尾部追加N
个元素:
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
不过要注意的是,在容量不足的情况下,append
的操作会导致重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用append
函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
由于append
函数返回新的切片,也就是它支持链式操作。我们可以将多个append
操作组合起来,实现在切片中间插入元素:
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
每个添加操作中的第二个append
调用都会创建一个临时切片,并将a[i:]
的内容复制到新创建的切片中,然后将临时创建的切片再追加到a[:i]
。
可以用copy
和append
组合可以避免创建中间的临时切片,同样是完成添加元素的操作:
a = append(a, 0) // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x // 设置新添加的元素
第一句append
用于扩展切片的长度,为要插入的元素留出空间。第二句copy
操作将要插入位置开始之后的元素向后挪动一个位置。第三句真实地将新添加的元素赋值到对应的位置。操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。
用copy
和append
组合也可以实现在中间位置插入多个元素(也就是插入一个切片):
a = append(a, x...) // 为x切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x) // 复制新添加的切片
稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。没有专门的内置函数用于扩展切片的容量,append
本质是用于追加元素而不是扩展容量,扩展切片容量只是append
的一个副作用。
删除切片元素
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。可以用append
原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
也可以用copy
完成删除开头的元素:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append
或copy
原地完成:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。