Golang学习-数组和切片

介绍Go中数组以及切片的使用和实现原理

# Array数组

Golang中的数组是静态的,存储着一段相同内容的连续空间。

# 基本使用

# 声明和初始化

在Go中数组的初始化主要有两种方式:一种是显示的指定数组大小,另一种是使用[...]自动推导数组大小的方式。

1
2
arr1 := [3]int{1,2,3}
arr2 := [...]int{1,2,3}

以上两种方式运行结果是一样的,后一种方式会在编译期间自动推导成第一种,只是Go为我们提供的语法糖。

Go编译器在初始化数组时,会根据字面量的多少来进行不同的优化:

  1. 当元素的数量小于或等于4个时,会将数组的元素直接放在栈上。

  2. 当元素的数量大于4个时,会将数组中元素放置在静态区并在运行时取出。

# 访问和读写

数组使用arr[n]来访问数组内元素,如果是一些简单的数组或者字符串的越界错误会在编译期间发现,而如果使用变量去访问数组,会在运行时触发程序的错误并导致崩溃退出。

# 多维数组使用

在Go多维数组的初始化和一维类似:

1
arr := [2][3]int{{1,2,3},{4,5,6}}

# Slice切片

# 定义和初始化

切片是Go提供的基于array的一种动态数组,其长度并不像数组那样固定,我们可以向切片中追加元素或者进行扩容等操作。

切片的初始化有三种方式:

  1. 通过下标初始化获得切片或者数组的一部分

  2. 使用字面量初始化新的切片

  3. 使用关键字make来创建切片

1
2
3
4
slice := arr[2:3]
slice := []int{1,2,3}
slice := make([]int,3) 
//make时还可以传入cap参数,当然,Go也会对参数进行校验,cap必须大于等于len

当Go编译器在创建切片时:

  1. 如果切片发生逃逸或者切片的大小或容量特别大时,需要在运行时在堆上创建底层数组和切片。

  2. 当切片特别小时,Go编译器会先在栈上或者静态存储区初始化数组,再通过下标(即第一种arr[2:3]的方式)得到切片。

在运行时创建切片时,编译器会计算切片所需要的空间并在堆上申请一片连续的内存空间(空间不足会panic):

内存大小=元素大小x切片容量

当内存分配完成,会返回底层数组的引用,并且和长度,容量合并成SliceHeader的结构体

切片的底层数据结构如下:

1
2
3
4
5
type SliceHeader struct {
  Data uintptr // 存储底层数组
  Len  int // 切片长度
  Cap  int // 切片分配的空间大小,小于Cap可以直接追加元素。
}

可以发现切片和数组主要不同在于cap字段。

切片实际上是在数组的基础上加了一层抽象层,切片实际上是底层数组的一个引用,再加上长度和容量,当我们在运行时修改切片的长度和容量时,底层的数组可能会发生变化,而在上层引用看来切片并没有发生变化。

切片和数组还有一点不同在于:切片只是在编译期间确定元素类型,而数组的编译期间就已经确定好了类型和长度。

# 切片的访问

切片通过下标去访问元素:

1
a := arr[3] //对切片的索引操作实际上会转化为地址的读取

在访问时,Go会进行边界检查,如果超出则会panic。

切片可以获取长度和容量:

1
2
3
arr := make([]int,10,20)
l := len(arr)
c := cap(arr)

# 切片追加和扩容

在Go中使用append关键字对切片进行扩容, 扩容后会产生一个新的slice结构体,如果赋值回去原变量就相当于对原变量进行了扩容。

1
2
arr := make([]int,3, 5)
arr = append(arr, []int{1, 2}...)

当切片追加元素时,会根据:

  1. 追加后切片长度小于等于容量

  2. 追加后切片长度大于容量

以及根据返回值是否覆盖原切片进行不同的流程

如果触发了第二种情况,即容量不足的情况,Go会对切片进行扩容,扩容其实是为切片分配新的内存空间并拷贝原切片中元素的过程。

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

Go 1.18之前:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量; Go 1.18之后: 切片扩容不再以1024为临界点,而是设定了一个值为256的threshold。在计算完容量之后,会根据容量和元素大小相乘,如果新的切片发生了内存溢出或者请求内存大于上限则会直接panic。
  4. 如果期望容量大于当前容量的两倍就会使用期望容量;
  5. 当原切片容量 < threshold 的时候,新切片容量变成原来的 2 倍;
  6. 当原切片容量 > threshold 的时候,进入一个循环,每次容量增加(旧容量+threshold*3) / 4;

# 切片拷贝

在Go中使用copy关键字进行切片的复制,实际上底层使用的是对内存的复制。

Go对切片仅支持appendcopy两种操作,需要注意的是在大切片中进行这两种操作会比较消耗资源。

# 案例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func func1(a []int) { 
    b := make([][]int, 0) 
    for i := 0; i < 10; i++ { 
        t := append(a, i) 
        b = append(b, t) 
    }
 }
// 需要理解到append的底层实现:
1. 判断原切片是否要扩容
2. 如果要扩容先new一个新的数组再copy值到新的数组如果不需要扩容直接追加元素
3. append操作实际上会生成一个slice结构体如果赋值回去原始切片会增加len如果不赋值回去则len不变
func func2() {
    a := [...]int{0, 1, 2, 3}
    x := a[:1]  // { pointer:->a len:1 cap: 4}       
    y := a[2:]          
    x = append(x, y...) 
    x = append(x, y...)
    fmt.Println(a, x)
}
//需要理解到[:]运算的本质
//实际上是创建一个切片的引用,底层数组还是指向原来切片,但len和cap发生变化。具体看是如何切割。
Licensed under CC BY-NC-SA 4.0