数组 逻辑定义
Go 数组:一个长度固定,由同构类型元素组成的连续序列
1 2 3 var arr [5 ]int = [5 ]int {1 , 2 , 3 , 4 , 5 }fmt.Println(arr)
数组元素的类型可以为任意 Go 原生类型或者自定义类型
数组的长度必须在声明数组变量时提供
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) fmt.Printf("%T\n" , arr2) fmt.Printf("%T\n" , arr3) f(arr1) f(arr2) f(arr3) }
内存表示
Go 编译器会为 Go 数组分配一整块可容纳所有元素的连续内存
数组长度
通过 len 函数获取一个数组变量的长度(元素数量),通过 unsafe.SizeOf 函数获取一个数组变量的总大小
1 2 3 arr := [5 ]int {1 , 2 , 3 , 4 , 5 } fmt.Printf("len: %v\n" , len (arr)) fmt.Printf("size: %v\n" , unsafe.Sizeof(arr))
初始化
声明一个数组变量时,如果不进行显式初始化,那么数组中的元素值就是对应的类型零值
1 2 3 4 5 var arr [5 ]int fmt.Printf("len: %v, arr: %#v\n" , len (arr), arr) var sl []int fmt.Printf("len: %v, sl: %#v\n" , len (sl), sl)
忽略数组长度,用 ... 代替,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) fmt.Printf("arr2 len: %v, type: %T\n" , len (arr2), arr2) fmt.Printf("arr3 len: %v, type: %T\n" , len (arr3), arr3)
对稀疏数组进行显式初始化,可以通过使用下标赋值的方式来简化代码
1 2 3 4 5 6 7 var arr = [...]int { 7 : 39 , 9 : 76 , } fmt.Printf("%#v\n" , arr) fmt.Printf("type: %T, len: %v, size: %v\n" , arr, len (arr), unsafe.Sizeof(arr))
数组访问
通过下标(从 0 开始)访问元素,非常高效,不存在 Go Runtime 带来的额外开销
1 2 3 4 var arr = [6 ]int {1 , 2 , 3 , 4 , 5 , 6 }fmt.Println(arr[0 ], arr[5 ]) fmt.Println(arr[-1 ]) fmt.Println(arr[6 ])
多维数组
递归
数组类型变量是一个整体,即一个数组变量表示的是整个数组
在 C 语言中,数组变量可视为指向数组第 1 个元素的指针
无论是参与迭代,或者作为实际参数传给一个函数或者方法
Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销
避免数组值拷贝带来的性能损耗
切片
Go 中最常用的同构复合类型
数组缺陷
固定的元素个数
值传递的开销较大
长度
无需在声明时指定长度,切片的长度是动态的,随着切片中元素的个数的变化而变化
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)) nums = append (nums, 7 ) fmt.Printf("len: %v, cap: %v\n" , len (nums), cap (nums))
实现原理
切片在运行时其实是一个三元组结构
1 2 3 4 5 type slice struct { array unsafe.Pointer len int cap int }
Field
Desc
array
指向底层数组的指针
len
切片的长度,即切片当前元素的个数
cap
切片的容量,即底层数组的长度,cap ≥ len
Go 编译器会自动为每个新建的切片,建立一个底层数组,默认底层数组长度与切片初始元素个数相同
创建切片 make
通过 make 函数来创建切片,并指定底层数组的长度
1 2 3 s := make ([]byte , 6 , 10 ) fmt.Printf("%#v\n" , s) fmt.Printf("len: %d cap: %d\n" , len (s), cap (s))
如果在 make 函数中没有指定 cap 参数,那么 cap == len
1 2 3 s := make ([]byte , 6 ) fmt.Printf("%#v\n" , s) fmt.Printf("len: %d cap: %d\n" , len (s), cap (s))
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) fmt.Printf("len: %d cap: %d\n" , len (sl), cap (sl))
切片 sl 的底层数组为 arr,修改切片 sl 中的元素将直接影响数组 arr
1 2 3 sl[0 ] += 10 fmt.Printf("%#v\n" , sl) fmt.Printf("%#v\n" , arr)
在 Go 中,数组更多承担底层存储空间的角色,切片为数组的描述符
因此,切片在函数参数传递时可以避免较大的性能开销,传递的大小的固定的(三元组)
可以在同一个底层数组上建立多个切片(共享同一个底层数组)
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) fmt.Printf("%#v\n" , sl2) fmt.Printf("%#v\n" , arr)
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 ] fmt.Printf("len: %d, cap: %d, sl1: %#v\n" , len (sl1), cap (sl1), sl1) fmt.Printf("len: %d, cap: %d, sl2: %#v\n" , len (sl2), cap (sl2), sl2) sl1[2 ] += 10 sl2[0 ] += 20 fmt.Printf("%#v\n" , sl1) fmt.Printf("%#v\n" , sl2) fmt.Printf("%#v\n" , arr)
动态扩容
通过 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) sl = append (sl, 1 ) fmt.Printf("len: %d, cap: %d\n" , len (sl), cap (sl)) sl = append (sl, 2 ) fmt.Printf("len: %d, cap: %d\n" , len (sl), cap (sl)) sl = append (sl, 3 ) fmt.Printf("len: %d, cap: %d\n" , len (sl), cap (sl)) sl = append (sl, 4 ) fmt.Printf("len: %d, cap: %d\n" , len (sl), cap (sl)) sl = append (sl, 5 ) fmt.Printf("len: %d, cap: %d\n" , len (sl), cap (sl))
扩容系数为 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) s := u[1 :3 ] fmt.Printf("len: %d, cap: %d, s: %#v\n" , len (s), cap (s), s) s = append (s, 14 ) fmt.Printf("len: %d, cap: %d, s: %#v\n" , len (s), cap (s), s) s = append (s, 15 ) fmt.Printf("len: %d, cap: %d, s: %#v\n" , len (s), cap (s), s) s = append (s, 16 ) fmt.Printf("len: %d, cap: %d, s: %#v\n" , len (s), cap (s), s) s[0 ] = 12 fmt.Printf("len: %d, cap: %d, s: %#v\n" , len (s), cap (s), s) fmt.Println(u)
对比
大多数场合,使用切片替代数组
切片作为数组的描述符,非常轻量
无论绑定的底层数组多大,传递切片的开销都是恒定可控的
切片 > 数组指针
切片支持下标访问、边界溢出校验、动态扩容等
指针本身在 Go 是受限的,如不支持指针的算术运算