Go语言中数组和切片笔记

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

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

数组和切片

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

原文发布于微信公众号 - 我的小碗汤(mysmallsoup)

原文发表时间:2018-06-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Go语言编程中字符串切割方法小结

1.func Fields(s string) []string,这个函数的作用是按照1:n个空格来分割字符串最后返回的是 []string的切片 import...

4389
来自专栏进击的君君的前端之路

firstElementChild、firstChild 、childNodes和children方法

1302
来自专栏Golang语言社区

深入剖析Go语言编程中switch语句的使用

switch语句可以让一个变量对反对值的列表平等进行测试。每个值被称为一个的情况(case),变量被接通检查每个开关盒(switch case)。 在Go编程,...

3657
来自专栏Golang语言社区

Golang语言社区--标准库strings包讲解

大家好,我是Golang语言社区主编彬哥,本篇文章是给大家转载关于标准库strings包的知识。

96016
来自专栏一个会写诗的程序员的博客

Scala类型推导Scala类型推导

根据Picrce的说法:“类型系统是一个可以根据代码段计算出来的值对它们进行分类,然后通过语法的手段来自动检测程序错误的系统。”

1502
来自专栏有趣的django

4.自定义序列类

1860
来自专栏飞雪无情的博客

Go语言实战笔记(五)| Go 切片

切片也是一种数据结构,它和数组非常相似,因为他是围绕动态数组的概念设计的,可以按需自动改变大小,使用这种结构,可以更方便的管理和使用数据集合。

1154
来自专栏三流程序员的挣扎

mermaid 语法

文字里用引号避免一些特殊字符的错误。比如矩形节点里有 () 时就无法渲染,所以加上引号。

1.8K3
来自专栏LeetCode

LeetCode 832. Flipping an Image

Given a binary matrix A, we want to flip the image horizontally, then invert it,...

390
来自专栏marsggbo

jquery的html,text,val

.html()用为读取和修改元素的HTML标签 .text()用来读取或修改元素的纯文本内容 .val()用来读取或修改表单元素的value值。 这三个方法功能...

2215

扫码关注云+社区

领取腾讯云代金券