前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中数组和切片笔记

Go语言中数组和切片笔记

作者头像
我的小碗汤
发布2018-08-22 11:12:42
3890
发布2018-08-22 11:12:42
举报
文章被收录于专栏:我的小碗汤我的小碗汤

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

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

数组和切片

数组(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的内容了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 进击云原生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档