Go语言中数组和切片笔记

今天有位大佬问我一个关于切片很简单的一个问题,却把我难住了,所以是时候了解下切片的底层了。

 

由于切片底层其实还是数组片段,所以先来对比看下,再逐步深入。

 

数组和切片

数组(array):数组长度定了以后不可变,值类型,也就是说当作为参数传递的时候,会产生一个副本。

 

切片(slice):定义切片时不用指定长度。切片是一个数组片段的描述,它包含了指向数组的指针ptr、数组实际长度len和数组最大容量cap。

 

Go语言中数组和切片笔记

 

 

定义数组:

 

//直接赋值并指定长度
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的内容了。

 

Go语言中数组和切片笔记

 

上一篇:【golang学习笔记】切片


下一篇:slice方法