slice 的本质
slice 本质上是基于数组实现的,slice 可以看作是由三个元素组成的结构体:
|
其中 ptr
是指向底层数组的指针, len 表示当前 slice 中元素的数量, cap 表示当前底层数组大小.
我们用 make 来创建 slice 的时候, 最多可以指定三个参数:
|
其中第三个参数是可选的, 用于指定底层数组的大小, 如果未指定, 则默认是和第二个参数是一致的.
用 make 创建的指定大小的数组会用类型的 0 值进行初始化, 例如对于下面的代码:
|
它的输出结果是 [0 0 2 3]
, 这是因为用 make 创建 slice 的时候, 里面已经存在了 2 个 0 值元素.
这里需要指出的一点是对 slice 的赋值操作是 O(1) 的, 它和底层数组的大小没有关系, 因为我们只需要把 (ptr, len, cap) 这三个值拷贝到新的 slice 即可.
子 slice
我们可以通过 slice[begin:end:cap_idx]
来获取一个子 slice, 子 slice 的大小是 end - begin
, 其中 end 和 cap_idx 最大可以设置为 cap(slice)
. 子 slice 相当于是:
- ptr = slice.ptr + begin
- len = end - begin
- cap = cap_idx - begin
当我们往 slice 中 append 数据时, 如果 slice 还有容量时, 直接 slice[len] = newValue
即可:
|
但如果 append 的数据超过当前 slice 的容量时, 便会重新申请一个数组存放要添加的数据. 例如我们往上面例子中的 slice 再添加一个新的数据时, 便会超过之前的容量而去重新申请一个数组. 这样之前数组里的内容便会还是默认值, 输出结果为: [0 0]
|
往slice里面添加数据
a := make([]int, 0, 10)
b := append(a, 1)
往slice a里面添加一个1的方式,就是上面的代码。不过实际使用中,我们常用的是这种方式:
a = append(a, 1)
那现在考虑这一行代码:
b := append(a, 1)
b和a的关系:如果len(a)+1<=cap(a),这个时候a内部的数组仍足够存储新添加的数据,此时,b的ptr和a的ptr是相同的。此时:b.ptr == a.ptr, len(b) == len(a)+1, cap(b) == cap(a)。
对一个slice重新切片
这里重新切片的意思,就是取slice里面的一部分元素。
a := make([]int, 10, 20)
b := a[0:5]
如上面这段代码,就是重新切片的。此时b.ptr == a.ptr。b.cap == a.cap。b.len == 5。
好了,了解了上面的基础属性,现在就可以开始练练手了。
1. 看看下面的代码会输出什么:
package main
func main() {
a := make([]int, 0)
b := append(a, 1)
_ = append(a, 2)
println(b[0])
}
我们往a里面添加了一个1成为了b。这个时候输出的是1,好像没什么问题。那下面这段代码会输出什么呢?
package main
func main() {
a := make([]int, 0, 10)
b := append(a, 1)
_ = append(a, 2)
println(b[0])
}
嗯,是2。这个我觉得就是使用slice的时候最大的坑。但理解了它们内部的存储方式,也就不难理解为什么是这样子了。
执行完:
b := append(a, 1)
此时b[0]的确是1。但此时b.ptr == a.ptr。因为这个时候cap(a)为10,足以存储新插入的元素1。
执行:
_ = append(a, 2)
此时,cap(a)仍然为10,len(a)仍然为0,往a里面插入元素2 ,使得ptr[0]==2。由于b.ptr与a.ptr相同,b里面的数据就被改掉了。
2. 看看下面的代码会输出什么:
package main
func main() {
a := make([]int, 10, 20)
b := a[5:]
println(len(b), cap(b))
}
答案是:5 15
输出5是因为a的长度为10,b := a[5:],相当于是对a重新切片,取a第5个元素以后的值。a第5个元素之后还剩下5个值,那len(b)就是5了。
cap(b)为什么为15呢?
因为此时, b.ptr = a.ptr + 5。也就是b内部指针,指向了a.ptr的后面第5个元素。所以此时cap(b)就不能是20了,因为b无法利用a前面的5个元素。
3. 如果避免重新切片之后的新切片,不被修改?如下所示:
package main
import (
"fmt"
)
func doAppend(a []int) {
_ = append(a, 0)
}
func main() {
a := []int{1, 2, 3, 4, 5}
doAppend(a[0:2])
fmt.Println(a)
}
这段代码会输出:
[1 2 0 4 5]
虽然我们调用doAppend的时候,只把2个元素传入了。但它却把a的第3个元素改掉了。如何避免呢?答案如下:
package main
import (
"fmt"
)
func doAppend(a []int) {
_ = append(a, 0)
}
func main() {
a := []int{1, 2, 3, 4, 5}
doAppend(a[0:2:2])
fmt.Println(a)
}
就是在对slice重新切片的时候,加入第三个capacity参数。
doAppend(a[0:2:2])
最后的2,就是指定了重新切片之后新的slice的capacity。我们指定它的capacity就是2,所以,doAppend函数进行append操作的时候,发现capacity不够3,就会重新分配内存。这时就不会修改原有slice的内容了。