今天有位大佬问我一个关于切片很简单的一个问题,却把我难住了,所以是时候了解下切片的底层了。
由于切片底层其实还是数组片段,所以先来对比看下,再逐步深入。
数组和切片
数组(array):数组长度定了以后不可变,值类型,也就是说当作为参数传递的时候,会产生一个副本。
切片(slice):定义切片时不用指定长度。切片是一个数组片段的描述,它包含了指向数组的指针ptr、数组实际长度len和数组最大容量cap。
定义数组:
//直接赋值并指定长度
p := [6]int{1, 2, 3, 4, 5, 6}
//申明时用...相当于指定了长度
pp := [...]int{1, 2, 3, 4, 5, 6}
定义切片:
p := [6]int{1, 2, 3, 4, 5, 6}
//使用p[from:to]进行切片,这种操作会返回一个新指向数组的指针,而不会复制数组
//p[from:to]表示从from到to-1的slice元素
x := p[1:4] //引用数组的一部分,即[2,3,4]
x := p[:3] //从下标0开始到下标2,即[1,2,3]
x := p[4:] //下标4截取到数组结尾,即[5,6]
//使用make函数创建slice
a := make([]int, 5) //len(a) = 5 和 cap(a) = 5
b := make([]int, 0, 5) //len(b) = 0 和 cap(b) = 5
用反射来看看类型:
package main
import (
"fmt"
"reflect"
)
func main() {
//定义一个类型为interface{}的切片
vslice := []interface{}{
[]int{}, //slice
[]int{1, 2, 3}, //slice
[]int{1, 2, 3}[:], //切片再切还是切片
make([]int, 3, 10), //标准的slice定义方式
[3]int{1, 2, 3}, //array数组,指定长度
[...]int{1, 2, 3}, //array数组,由编译器自动计算数组长度
}
for i, v := range vslice {
rv := reflect.ValueOf(v)
fmt.Println(i, "---", rv.Kind())
}
}
运行输出:
0 --- slice
1 --- slice
2 --- slice
3 --- slice
4 --- array
5 --- array
Process finished with exit code 0
我们可以得出结论,slice和array的区别:
-
数组申明需要在方括号中指定长度或者用...隐式指明长度,而且是值传递,作为参数传递会复制出一个新的数组;
-
切片不用再方括号指定长度,从底层来看,切片实际上还是用数组来管理,其结构可以抽象为ptr:指向数组的指针、len:数组实际长度、cap:数组的最大容量,有了切片,我们可以动态扩容,并替代数组进行参数传递而不会复制出新的数组。
在使用slice时,几点注意事项:
1、对slice进行切分操作
对slice进行切分操作会生成一个新的slice变量,新slice和原来的slice指向同一个底层数组,只不过指向的起始位置可能不同,长度及容量可能也不相同。
当从左边界有截断时,会改变新切片容量大小;
左边界默认0,最小为0;右边界默认slice的长度,最大为slice的容量;
当然,因为指向同一个底层数组,对新slice的操作会影响到原来的slice。
package main
import "fmt"
func main() {
p := make([]int, 0, 6)
p = append(p , 1, 2, 3, 4, 5)
//p is [1 2 3 4 5] len(p) is 5 cap(p) is 6
fmt.Println("p is", p, "len(p) is ", len(p), "cap(p) is ", cap(p))
//截断左边界,改变了新切片容量大小
a := p[1:]
//a is [2 3 4 5] len(a) is 4 cap(a) is 5
fmt.Println("a is", a, "len(a) is ", len(a), "cap(a) is ", cap(a))
//左边界默认0,右边界默认为len(p)
b := p[:]
//b is [1 2 3 4 5] len(b) is 5 cap(b) is 6
fmt.Println("b is", b, "len(b) is ", len(b), "cap(b) is ", cap(b))
//右边界最大为slice的容量p[:6]
c := p[:cap(p)]
//p[:6]即取p[0]~p[5],由于len(p)为5,所以最后一个是int默认值0
//c is [1 2 3 4 5 0] len(c) is 6 cap(c) is 6
fmt.Println("c is", c, "len(c) is ", len(c), "cap(c) is ", cap(c))
//由于底层指向同一个数组,所以改变b[0],c切片中c[0]也被改变
b[0] = 100
//c is [100 2 3 4 5 0]
fmt.Println("c is", c)
}
运行结果如下:
p is [1 2 3 4 5] len(p) is 5 cap(p) is 6
a is [2 3 4 5] len(a) is 4 cap(a) is 5
b is [1 2 3 4 5] len(b) is 5 cap(b) is 6
c is [1 2 3 4 5 0] len(c) is 6 cap(c) is 6
c is [100 2 3 4 5 0]
Process finished with exit code 0
2、切片的扩容:
利用切片名字加下标的方式赋值时,当下标大于等于len时会报“下标越界”,需要用内置函数append来赋值。
package main
import "fmt"
func main() {
p := make([]int, 3, 5)
p[0] = 1
p[1] = 2
p[2] = 3
//p[3]=4 //这样赋值会报错:panic:runtime error:index out of range
fmt.Println(cap(p)) //5,容量够用
p = append(p, 5, 6, 7) //需要用append,append在存储空间不足时,会自动增加存储空间
fmt.Println(cap(p)) //10,存7的时候容量5不够用,扩容到原来容量的2倍
fmt.Println(p) //[1,2,3,5,6,7]
}
如果要动态增加slice的容量,则需要新建一个slice并把旧slice的数据复制过去:
package main
import "fmt"
func main() {
s := make([]int, 0, 10)
s = append(s, 1, 2, 3, 4, 5)
//before s = [1 2 3 4 5] cap(s) is 10 len(s) is 5
fmt.Println("before s =", s, "cap(s) is ", cap(s), "len(s) is ", len(s))
//把s扩容两倍
d := make([]int, len(s), (cap(s)+1)*2) //加1是为了防止cap(s)==0这种情况
copy(d, s) //使用内建函数copy复制slice
s = d
//after s = [1 2 3 4 5] cap(s) is 22 len(s) is 5
fmt.Println("after s =", s, "cap(s) is ", cap(s), "len(s) is ", len(s))
}
3、slice 的零值
slice 的零值是 nil,一个 nil 的 slice 的len和cap是 0。
package main
import "fmt"
func main() {
var s []int //定义一个空的指针,s==nil,len(s)=0, cap(s)=0
fmt.Println("s is ", s, "len(s) is ", len(s), "cap(s) is ", cap(s))
if nil == s {
fmt.Println("true")
}
s = make([]int, 0, 10) //分配内存
s = append(s, 1, 2, 3, 4, 5)
}
输出如下:
s is [] len(s) is 0 cap(s) is 0
true
Process finished with exit code 0
来看几个问题
问题一:
如下代码输出结果是什么?
package main
import "fmt"
func main() {
p := []int{1,2,3}
a := p[:2][1:][:2]
fmt.Println(a)
}
首先p[:2]切片后结果是[1,2],此时len为2,cap还是3;然后[1:]切片后是[2],此时len为1,cap为2;此时ptr已经指向p[1]了,然后再[:2]切片,即取p[1:3],所以结果是[2,3],len(a)为2,cap(a)为2。该问题主要得明白,3次切片后都指向同一个底层数组。
运行输出结果:
[2 3] len(a) is 2 cap(a) is 2
Process finished with exit code 0
问题二:
以下程序运行收输出什么?
package main
func main() {
a := make([]int, 0)
b := append(a, 1)
_ = append(a, 2)
println(b[0])
}
由于a切片的len和cap都为0,当执行b := append(a, 1)后其实b已经扩容重新分配内存了,而后面执行的_ = append(a, 2)自然对b没有影响,所以结果输出1。
那么以下这个程序输出又是什么呢?
package main
func main() {
a := make([]int, 0, 10)
b := append(a, 1)
_ = append(a, 2)
println(b[0])
}
结果是2。这个我觉得就是使用slice的时候最大的坑。但理解了它们内部的存储方式,也就不难理解了。a是len为0,cap为10的切片,执行完b := append(a, 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了,但是此时a的len还是0。
问题三:
如何避免重新切片之后的新切片不被修改?
如下代码:
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]
Process finished with exit code 0
虽然我们调用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参数2,意思就是指定了重新切片之后新的slice的capacity。我们指定它的capacity就是2,所以,doAppend函数进行append操作的时候,发现capacity不够3,就会重新分配内存。这时就不会修改原有slice的内容了。