数组

逻辑定义

Go 数组:一个长度固定,由同构类型元素组成的连续序列

1
2
3
// 声明了一个数组变量 arr,其类型为 [5]int,其中元素的类型为 int,数组的长度为 5
var arr [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr) // [1 2 3 4 5]
  1. 数组元素的类型可以为任意 Go 原生类型或者自定义类型
  2. 数组的长度必须在声明数组变量时提供
    • Go 编译器需要在编译阶段就知道数组类型的长度(整型数字字面值 or 常量表达式)

如果两个数组类型的元素类型 T数组长度 N 都是一样的,那么两个数组类型等价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func f(arr [5]int) {
}

func main() {
var arr1 [5]int
var arr2 [6]int
var arr3 [5]string

fmt.Printf("%T\n", arr1) // [5]int
fmt.Printf("%T\n", arr2) // [6]int
fmt.Printf("%T\n", arr3) // [5]string

f(arr1)
f(arr2) // cannot use arr2 (variable of type [6]int) as [5]int value in argument to f
f(arr3) // cannot use arr3 (variable of type [5]string) as [5]int value in argument to f
}

内存表示

Go 编译器会为 Go 数组分配一整块可容纳所有元素的连续内存

image-20231105170259094

数组长度

通过 len 函数获取一个数组变量的长度(元素数量),通过 unsafe.SizeOf 函数获取一个数组变量的总大小

1
2
3
arr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("len: %v\n", len(arr)) // len: 5
fmt.Printf("size: %v\n", unsafe.Sizeof(arr)) // size: 40,在 64 bit 平台,int 为 8 Bytes

初始化

声明一个数组变量时,如果不进行显式初始化,那么数组中的元素值就是对应的类型零值

1
2
3
4
5
var arr [5]int
fmt.Printf("len: %v, arr: %#v\n", len(arr), arr) // len: 5, arr: [5]int{0, 0, 0, 0, 0}

var sl []int
fmt.Printf("len: %v, sl: %#v\n", len(sl), sl) // len: 0, sl: []int(nil)

忽略数组长度,用 ... 代替,Go 编译器会根据数组初始化元素的个数,自动计算出数组长度

1
2
3
4
5
6
7
var arr1 [5]int = [5]int{1, 2, 3, 4, 5}
var arr2 = [5]int{1, 2, 3, 4, 5}
var arr3 = [...]int{1, 2, 3, 4, 5} // 编译器自动计算

fmt.Printf("arr1 len: %v, type: %T\n", len(arr1), arr1) // arr1 len: 5, type: [5]int
fmt.Printf("arr2 len: %v, type: %T\n", len(arr2), arr2) // arr2 len: 5, type: [5]int
fmt.Printf("arr3 len: %v, type: %T\n", len(arr3), arr3) // arr3 len: 5, type: [5]int

稀疏数组进行显式初始化,可以通过使用下标赋值的方式来简化代码

1
2
3
4
5
6
7
var arr = [...]int{
7: 39,
9: 76,
}

fmt.Printf("%#v\n", arr) // [10]int{0, 0, 0, 0, 0, 0, 0, 39, 0, 76}
fmt.Printf("type: %T, len: %v, size: %v\n", arr, len(arr), unsafe.Sizeof(arr)) // type: [10]int, len: 10, size: 80

数组访问

通过下标(从 0 开始)访问元素,非常高效,不存在 Go Runtime 带来的额外开销

1
2
3
4
var arr = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println(arr[0], arr[5]) // 1 6
fmt.Println(arr[-1]) // invalid argument: index -1 (constant of type int) must not be negative
fmt.Println(arr[6]) // invalid argument: index 6 out of bounds [0:6]

多维数组

递归

1
var mArr [2][3][4]int

marr

  1. 数组类型变量是一个整体,即一个数组变量表示的是整个数组

  2. 在 C 语言中,数组变量可视为指向数组第 1 个元素的指针

  3. 无论是参与迭代,或者作为实际参数传给一个函数或者方法

    • Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销
  4. 避免数组值拷贝带来的性能损耗

    • 类 C:可以通过使用指针 的方式,来向函数传递数组

    • 切片

切片

Go 中最常用同构复合类型

数组缺陷

  1. 固定的元素个数
  2. 值传递开销较大

长度

无需在声明时指定长度,切片的长度是动态的,随着切片中元素的个数的变化而变化

1
2
3
4
5
var nums = []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len: %v, cap: %v\n", len(nums), cap(nums)) // len: 6, cap: 6

nums = append(nums, 7)
fmt.Printf("len: %v, cap: %v\n", len(nums), cap(nums)) // len: 7, cap: 12

实现原理

切片在运行时其实是一个三元组结构

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}
Field Desc
array 指向底层数组的指针
len 切片的长度,即切片当前元素的个数
cap 切片的容量,即底层数组的长度,cap ≥ len

Go 编译器自动为每个新建的切片,建立一个底层数组,默认底层数组长度切片初始元素个数相同

slice

创建切片

make

通过 make 函数来创建切片,并指定底层数组长度

1
2
3
s := make([]byte, 6, 10)
fmt.Printf("%#v\n", s) // []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
fmt.Printf("len: %d cap: %d\n", len(s), cap(s)) // len: 6 cap: 10

如果在 make 函数中没有指定 cap 参数,那么 cap == len

1
2
3
s := make([]byte, 6)
fmt.Printf("%#v\n", s) // []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
fmt.Printf("len: %d cap: %d\n", len(s), cap(s)) // len: 6 cap: 6

from array

数组切片化:array[low:high:max]max默认值为数组的长度,规则为左闭右开

1
2
3
4
5
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]

fmt.Printf("%#v\n", sl) // []int{4, 5, 6, 7}
fmt.Printf("len: %d cap: %d\n", len(sl), cap(sl)) // len: 4 cap: 6

切片 sl 的底层数组为 arr,修改切片 sl 中的元素将直接影响数组 arr

1
2
3
sl[0] += 10
fmt.Printf("%#v\n", sl) // []int{14, 5, 6, 7}
fmt.Printf("%#v\n", arr) // [10]int{1, 2, 3, 14, 5, 6, 7, 8, 9, 10}
  1. 在 Go 中,数组更多承担底层存储空间的角色,切片为数组的描述符
  2. 因此,切片在函数参数传递时可以避免较大的性能开销,传递的大小的固定的(三元组

可以在同一个底层数组上建立多个切片(共享同一个底层数组)

1
2
3
4
5
6
7
8
9
10
11
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

sl1 := arr[3:7:9]
sl2 := arr[1:4]

sl1[1] += 20
sl2[2] += 10

fmt.Printf("%#v\n", sl1) // []int{14, 25, 6, 7}
fmt.Printf("%#v\n", sl2) // []int{2, 3, 14}
fmt.Printf("%#v\n", arr) // [10]int{1, 2, 3, 14, 25, 6, 7, 8, 9, 10}

image-20231105200719669

from slice

原理与 from array 一样,共享同一个底层数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

sl1 := arr[3:7:8]
sl2 := sl1[1:3] // max 的默认值为 sl1 的 max

fmt.Printf("len: %d, cap: %d, sl1: %#v\n", len(sl1), cap(sl1), sl1) // len: 4, cap: 5, sl1: []int{4, 5, 6, 7}
fmt.Printf("len: %d, cap: %d, sl2: %#v\n", len(sl2), cap(sl2), sl2) // len: 2, cap: 4, sl2: []int{5, 6}

sl1[2] += 10
sl2[0] += 20

fmt.Printf("%#v\n", sl1) // []int{4, 25, 16, 7}
fmt.Printf("%#v\n", sl2) // []int{25, 16}
fmt.Printf("%#v\n", arr) // [10]int{1, 2, 3, 4, 25, 16, 7, 8, 9, 10}

动态扩容

通过 append 操作向切片追加数据时
如果 len == cap,则 Go Runtime 会对这个切片做动态扩容重新分配底层数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var sl []int
fmt.Printf("len: %d, cap: %d, v: %#v\n", len(sl), cap(sl), sl) // len: 0, cap: 0, v: []int(nil)

sl = append(sl, 1)
fmt.Printf("len: %d, cap: %d\n", len(sl), cap(sl)) // len: 1, cap: 1

sl = append(sl, 2)
fmt.Printf("len: %d, cap: %d\n", len(sl), cap(sl)) // len: 2, cap: 2

sl = append(sl, 3)
fmt.Printf("len: %d, cap: %d\n", len(sl), cap(sl)) // len: 3, cap: 4

sl = append(sl, 4)
fmt.Printf("len: %d, cap: %d\n", len(sl), cap(sl)) // len: 4, cap: 4

sl = append(sl, 5)
fmt.Printf("len: %d, cap: %d\n", len(sl), cap(sl)) // len: 5, cap: 8

扩容系数为 2,新数组建立后,append 会把旧数组中的数据拷贝到新数组,旧数组会被 GC

自动扩容 -> 解除绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
u := [...]int{1, 2, 3, 4, 5}
fmt.Println(u) // [1 2 3 4 5]

s := u[1:3]
fmt.Printf("len: %d, cap: %d, s: %#v\n", len(s), cap(s), s) // len: 2, cap: 4, s: []int{2, 3}

s = append(s, 14)
fmt.Printf("len: %d, cap: %d, s: %#v\n", len(s), cap(s), s) // len: 3, cap: 4, s: []int{2, 3, 14}

s = append(s, 15)
fmt.Printf("len: %d, cap: %d, s: %#v\n", len(s), cap(s), s) // len: 4, cap: 4, s: []int{2, 3, 14, 15}

s = append(s, 16) // new array is created
fmt.Printf("len: %d, cap: %d, s: %#v\n", len(s), cap(s), s) // len: 5, cap: 8, s: []int{2, 3, 14, 15, 16}

s[0] = 12
fmt.Printf("len: %d, cap: %d, s: %#v\n", len(s), cap(s), s) // len: 5, cap: 8, s: []int{12, 3, 14, 15, 16}
fmt.Println(u) // [1 2 3 14 15]

对比

大多数场合,使用切片替代数组

  1. 切片作为数组的描述符,非常轻量
    • 无论绑定的底层数组多大,传递切片的开销都是恒定可控
  2. 切片 > 数组指针
    • 切片支持下标访问边界溢出校验动态扩容
    • 指针本身在 Go 是受限的,如不支持指针的算术运算