转载地址
本文的灵感来自于某篇公众号文章:https://mp.weixin.qq.com/s?__biz=MjM5MDUwNTQwMQ==&mid=2257483743&idx=1&sn=af5059b90933bef5a7c9d491509d56d9&scene=21#wechat_redirect
什么是slice
slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。
slice的创建
通过字面量创建的一个特殊形式:索引
截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。
基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。
值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。
下面代码会输出什么?
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3, 8:100}
fmt.Println(s1, len(s1), cap(s1))
s2 := []int{5:128}
fmt.Println(s2, len(s2), cap(s2))
}
答案是:
[0 1 2 3 0 0 0 0 100] 9 9 //下标为8的元素是100
[0 0 0 0 0 128] 6 6 //下标为5的元素是128
上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值。
切片的截取与长度、容量的关系
下面代码输出什么?
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
答案如下:
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
解决这个问题的关键先看切片初始的长度和容量
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
fmt.Printf("slice的长度=%d,容量=%d \n",len(slice),cap(slice))//slice的长度=10,容量=10
fmt.Printf("s1的长度=%d,容量=%d \n",len(s1),cap(s1))//s1的长度=3,容量=8
fmt.Printf("s2的长度=%d,容量=%d \n",len(s2),cap(s2))//s2的长度=4,容量=5
}
s1的长度是slice切片下标2到下标4的元素的长度,为3;容量是下标2到结尾,为8。
s2的长度是s1切片下标2到下标5的元素的长度,为4;容量从下标2到索引7(开区间,真正到索引6),为5。
这两个切片都共用同一个底层数组。
接着,向 s2 尾部追加一个元素 100:s2 = append(s2, 100)
s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。
再次向 s2 追加元素200:s2 = append(s2, 200)
这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。
最后,修改 s1 索引为2位置的元素:s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。
再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。
总结:
•切片是对底层数组的一个抽象,描述了它的一个片段。
•切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
•多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
•append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
•扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
•当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。