• Home
  • About
    • Xrlin photo

      Xrlin

      A blog for sharing my thoughts and knowledge

    • Learn More
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

Golang中的数组与slice

09 Mar 2018

Reading time ~3 minutes

  • 数组
    1. 定义 数组是很常用的一种数据结构,go中的数组定义与c类似, 如c中用int[10]表示一个长度为10的数组,而go用[10]int来表示,只是将类型声明放在后面,go还提供很多方便的数组定义方法。

      // 数组定义
      
      // 定义一个长度为0的数组,数组的内容初始话为0(int类型的零值)
      var a [10]int
      // 定义并初始化数组的前2个值,数组初始化为[1 3 0 0 0 0 0 0 0 0]
      a := [10]int{1,2}
      // 下面两条语句都定义一个长度为0的数组,并依次初始化
      a := [10]int{1,2,3,4,5,,6,7,8,9,10}
      a := [...]int{1,2,3,4,5,,6,7,8,9,10}
      
      // 数组长度 10
      fmt.Prinln(len(a))
      // 数组容量 10
      fmt.Pringln(cap(a))
      
    2. 数组的修改

       var a [10]int
       // 赋值
       a[0] = 1
       // 编译不通过, 超出数组的长度
       a[10] = 11
       // 编译不通过,数组不能进行扩展
       a = append(a, 1)
      
    3. 数组作为函数参数

      // 数组作为函数参数时属于值传递,函数内的数组是参数的一个值复制,不同于java、c++等面向对象语言中数组参数的处理
      var a [10]int
      func test(arr [10]int) {
          fmt.Printf("%p\n", &arr)
          arr[0] = 1
      }
      
      fmt.Println(a) // [0 0 0 0 0 0 0 0 0 0]
      fmt.Printf("%p\n", &a) // 0x10450030 
      test(a)               // 0x10450060, 可以看出a与方法中arr所指向的地址是不一样的,arr是a的一个完整拷贝
      fmt.Println(a) // [0 0 0 0 0 0 0 0 0 0]
      
      // 使用指针作为参数,c、c++中的指针概念依旧可以用在go中,当需要在其它命名空间(如方法)中修改参数对应的外部变量值时,影使用指针作为参数
      func test2(arr *[10]int) {
          (*arr)[0] = 1
      }
      fmt.Println(a) // [0 0 0 0 0 0 0 0 0 0]
      test2(&a)
      fmt.Println(a)  // [1 0 0 0 0 0 0 0 0 0]
      
  •   Slice(切片)
    1. 定义及初始化 slice在go中是常用的数据结构,在go中它经常用来代替数组进行使用,对于写过python的人来说,slice这词应该不陌生,python中的slice经常用于截取一部分数组、字符串生成新的一个对象,但在go中它实际上是数组一部分范围的表示。因为slice可以使用append函数进行扩充,slice的初始化与数组类似。
      // 初始化长度为0的空slice
      var a []int
      a[0] = 1 // panic: runtime error: index out of range, 下标超出数组长度
      // 初始化一个数组并赋值
      a := []int{1,2}
      // 初始化一个长度为10, 容量为10的数组
      a := make([]int, 10)
      // 初始化一个长度为10, 容量为20的数组
      a := make([]int, 10, 20)
      a[11] = 10 // panic: runtime error: index out of range, slice不能自动扩展长度
      // 使用append扩展slice
      a = append(a, 1)
      fmt.Println(a, len(a), cap(a)) // [0 0 0 0 0 0 0 0 0 0 1] 11 20
      a = append(a, []int{2,3,4,5,6,7,8,9,10}...)
      fmt.Println(a, len(a), cap(a)) // [0 0 0 0 0 0 0 0 0 0 1] 11 20
      a = append(a, 21)
      // 在数组超出cap容量后,会自动分配新的数组,并设置新的cap,cap的分配规则由golang runtime决定,
      // 注意该分配规则没有在golang spec中声明,使用时不应对append后的cap进行假设
      fmt.Println(a, len(a), cap(a)) // [0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9 10 21] 21 40
      
    2. slice的底层数据结构   slice故名思意就是对数组的切片,在go的实现中,slice的数据由header和底层数组array组成。
      // Header
      type sliceHeader struct {
          Length        int
          Capacity      int
          ZerothElement *byte // 该指针类型由slice的元素类型决定
      }
      

      这只是为了方便理解对底层实现的一个抽象, Length、Capacity分别表示slice的长度、容量,ZerothElement对应底层数组中slice起始元素的指针,正因为slice的结构组成,所以你可能会遇到下面的一些情况。

      a := [10]int{1,2,3,4,5,6,7,8,9,10}
      s := a[2:5]
      // a成为s的底层数组,ZerothElement指向a[2],a[2]之后长度为8,即s的容量为8
      fmt.Println(s, len(s), cap(s)) // [3 4 5] 3 8
      a[2] = 21
      fmt.Println(s) // [21, 4, 5]
      s := append(s, 22)
      fmt.Println(s) // [21, 4, 5, 22]
      fmt.Println(a) // [1 2 21 4 5 22 7 8 9 10]
      a[4] = 23
      // 因为a此时是s的底层数组,对s或底层数组a的更改都会影响到s、a两个值
      fmt.Println(s) // [21, 4, 23, 22]
      
      // 现在看下另一种情况
      s = append(s, []int{1,2,3,4}...)
      // 此时s的已满,达到最大容量
      fmt.Println(s, len(s), cap(s)) // [21 4 23 22 1 2 3 4] 8 8
      fmt.Println(a)
      // 继续加入元素
      s = append(s, 33)
      // slice已满,再次添加元素会自动扩容
      fmt.Println(s, len(s), cap(s))  // [21 4 23 22 1 2 3 4 33] 9 16
      fmt.Println(a)  // [1 2 21 4 23 22 1 2 3 4], 数组a没有改变,说明此时s的底层数组已经不是a了
      
      // 对数组a的操作不会影响到s
      a[2] = 3333
      fmt.Println(a) // [1 2 3333 4 23 22 1 2 3 4]
      fmt.Println(s) // [21 4 23 22 1 2 3 4 33]
      
      // 对s的操作不会影响到a
      s[2] = 22222
      fmt.Println(a) // [1 2 3333 4 23 22 1 2 3 4]
      fmt.Println(s) // [21 4 22222 22 1 2 3 4 33]
      

      对于新手来说,slice和数组的区别很容易造成程序的bug,但只要明白slice的内部数据结构便可以轻松应对,对于需要append操作的slice,不能依赖于原有数组的值,同时避免修改其初始化时的数组,避免相互影响。

    3. slice作为函数参数 和数组一样,slice也可以用作函数的参数,同样slice作为函数的参数也是属于值传递(实际上golang的所有参数传递都是值传递),但传递的不是slice的深拷贝,而是slice的header,首先会将slice的header复制一份,然后传入函数中,但由于header中存在ZerothElemen这个指针,在函数内对于slice中元素值的变动会影响到原有slice的值,但如果对slice有append操作时则情况会不一样,如果append后slice的底层数组不改变,则还会影响到原有slice的值,但如果append导致底层数组改变了,之后(包括此次append)对函数内slice的变动不会影响到原有slice,因为底层数组不一样了,下面就是一个例子。
      slice1 := make([]int, 1, 1)
      
      func changeValue(a []int) {
          a[0] = 1
      }
      
      func appendValue(a []int) {
          a = append(a, 2)
      }
      
      fmt.Println(a)  // [0]
      changeValue(slice1)
      fmt.Println(slice1) // [1], 底层数组的指针没有改变,改变了原来slice1的值
      appendValue(slice1)
      fmt.Println(slice1) // [1], appendValue方法中a底层数组指针相对于slice1改变了,对a的更改不会影响到slice1
      

      如果确定要在函数中同步更新原slice的值,并且可能会涉及到append操作,这时可以传递一个slice的指针(其实是slice header的指针),通过对指针更新会更新原slice的header及底层数组,因而实现同步更新。

      func changeSlice(p *[]int) {
          a := *p
          a = append(a, 2)
          *p = a[:]
      }
      
      slice1 := make([]int, 1, 1)
      changeSlice(&slice1)
      fmt.Println(slice1) // [1 2]
      


go Like Tweet +1