数组和切片的比较 (Golang)

前言:写了数组和切片基本的特性,还有切片共享数据的一些坑,平时应该多加小心

数组

数组是一个固定长度的序列

数组变量的含义

举个栗子:

var a =[...]int{0,1,2}
fmt.Println(a, &a, &a[0], &a[1], &a[2])

输出的是:
[0 1 2] &[0 1 2] 0xc00008e000 0xc00008e008 0xc00008e010

要注意的是,一个数组变量表示的是一个完整的值,代表整个数组,并不是指向第一个元素的指针

这一点与 C/C++ 的数组不同,C/C++ 的数组变量是(隐式的)指向第一个元素的指针

int a[3] = {0,1,2};
cout<< a << endl << &a << endl << &a[0];

0x7ffee6b2568c
0x7ffee6b2568c
0x7ffee6b2568c

golang 中,为了避免数组赋值时产生较大开销,一般会创建一个指针指向这个数组

数组的拷贝

另外一个与 C/C++ 不同的点是,golang 的赋值都是值拷贝,数组的拷贝当然也是

var a = [3]int{0,1,2}
var b = a

会发现拷贝之后的数组 b 不仅值与 a 数组相同,b 中元素的地址也与 a 相同

一些操作

  • 数组的定义:

      // 字符串数组
      var s = [...]string{}
    
      // 结构体数组
      var structList = [...]struct.Val{struct.Val:0}
    
      // 接口数组
      var interfaceList = [...]interface{}{1, "a"}
    
      // 管道数组
      var chanList = [3]chan int{}
    
  • 遍历数组:

      for i := range a {
          ...
      }
    
      也可以 for i := 0; i < len(a); i++ {}
    
      for range 性能更好,而且可以保证不会出现数组越界
    
  • 内置函数:

    • len() 用于计算数组的长度
    • cap() 用于计算数组的容量(元素个数)

切片 slice

切片是可以动态增长和收缩序列,可以认为是动态数组

切片变量的含义

切片其实是一个结构体:

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}
  • Data 是切片指向的低层字节数组(当 Data 指针为空时,切片为 nil)
  • Len 是切片的字节长度
  • Cap 是切片指向的内存空间的最大容量(元素个数)

那么一个切片变量是表示的是什么呢?它表示的是 Data 指针指向的低层数组的值

var c = []int{1,2}
fmt.Println(c, &c, &c[0])

[1 2] &[1 2] 0xc000086000

要时刻记住一个重要的特点:slice 是对低层数组的引用

也正是因为这个特性,使得 slice 具有共享数据的特点

共享

因为切片是引用类型,多个切片如果指向同一个数组的片段,它们可以共享数据

举个例子:

func mulA(a [5]int) {
    for i, _ := range a {
        a[i] *= 2
    }
}

func mulB(a []int) {
    for i, _ := range a {
        a[i] *= 2
    }
}

func main() {
    a := [5]int{1, 2, 3, 4, 5}
    b := []int{1, 2, 3, 4, 5}
    mulA(a)
    fmt.Println(a) 

    mulB(b)
    fmt.Println(b)
}

输出的结果是

[1 2 3 4 5]
[2 4 6 8 10]

我们可以看到,调用 Mul 函数之后,数组 a 原本的值并没有改变,而 slice b 原本的值改变了,原因就在于 slice 是一个引用类型

slice 允许多个 slice 指向同一个底层数组,在很多场景下都能通过这个特性实现 no copy,不需要使用额外的内存而提高效率

但是!!!共享同时意味着不安全,容易掉进一些意想不到的坑里,再看一个例子

我们先定义了一个 slice a,然后想在 a[0:1] 后面追加一个元素 3 再赋值给 b,代码如下:

var a = []int{1,2}
b := append(a[0:1], 3)

fmt.Println(a)
fmt.Println(b)

按照我们原本的想法:
a = [1, 2]
b = [1, 3]

结果却输出了:
a = [1, 3]
b = [1, 3]

这是因为 a, b 都指向同一个低层数组,b 在追加元素 3 时覆盖了这个数组的第二个元素,导致打印 slice a 时,输出的也是 [1, 3]

那么正确的写法应该是怎么样的?

我们的需求是希望 a, b 互不影响,那么 b 是一个新的 slice,再通过 copy 把 a 的内容赋值给 b

var a = []int{1, 2}

b := make([]int, 2)
copy(b, a[0:1])
b = append(b, 3)

切片的定义

var (
    a  = []int{1, 2, 3, 4} //len和cap都是4

    b = a[:2] //取a开头两个元素, len=2, cap(b)=cap(a)

    c = a[1:2:4] //a 的 index 从 1到2(左边界闭,右边界开),cap=4-1, len=1

    d = make([]int, 2, 3) // len=2, cap=3
)

切片的操作

  • 添加切片元素:

      // 在首部添加元素 1
      a = append(1, a)
    
      // 在尾部添加元素 1
      a = append(a, 1)
    
      // 可添加切片
      a = append(a, []int{1,2,3})
    

    PS: 调用 append() 函数之后,返回一个新的切片,用这个新的切片来更新原有的切片

  • 复制

      copy(b[i:], a[j:])
    
      copy(a[i+1:], a[i:])
    
  • 删除元素

      // 删除开头一个元素
      a = a[1:]
    
      // 删除末尾一个元素
      a = a[:len(a)-1]
    
      // 删除 i 到 j 中的元素
      a = append(a[:i], a[j:]...)