Golang 切片(Slice)和数组
// 数组: new([len]Type)
arr := new([5]int)
arr := [5]int{1, 2} // [5]int{1, 2, 0, 0, 0}
arr := [5]{1, 2, 3, 4, 5}
arr := [...]{1, 2, 3, 4, 5} // [5]int{1, 2, 3, 4, 5}
arrKV := [...]int{1: 10, 6: 20, 30} // [8]int{0, 10, 0, 0, 0, 0, 20, 30}
// 切片: make([]Type, size[, cap])
slice := make([]int, 0) // []int{}
slice := make([]int, 5) // []int{0, 0, 0, 0, 0}
slice := make([]int, 5, 10) // []int{0, 0, 0, 0, 0}
// 在预先知道所需切片大小时可以预先分配好底层数组, 避免 append 时频繁扩容
slice := make([]int, 0, 100) // []int{}
// 从现有数组/切片中截取: arr[start:end:max]
// 其中: start <= end <= max <= len(arr)
arr := [20]int{}
slice := arr[1:5:10]
1. 数组[1]
1.1 概念
数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 [5]int
和 [10]int
是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的。
数组元素可以通过 索引(位置)来读取(或者修改),索引从 0
开始,第一个元素索引为 0
,第二个索引为 1
,以此类推(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目(也称为长度或者数组大小)必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2GB。
声明的格式是:
var identifier [len]type
例如:
var arr1 [5]int
Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new()
来创建: var arr1 = new([5]int)
。
那么这种方式和 var arr2 [5]int
的区别是什么呢?arr1
的类型是 *[5]int
,而 arr2
的类型是 [5]int
。
1.2 数组常量
如果数组值已经提前知道了,那么可以通过 数组常量 的方法来初始化数组,而不用依次使用 []=
方法(所有的组成元素都有相同的常量语法)。
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22}
var arrLazy1 = []int{5, 6, 7, 8, 22} //注:初始化得到的实际上是切片slice
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
var arrKeyValue1 = []string{3: "Chris", 4: "Ron"} //注:初始化得到的实际上是切片slice
第一种变化:
var arrAge = [5]int{18, 20, 15, 22, 16}
注意 [5]int
可以从左边起开始忽略:[10]int {1, 2, 3}
:这是一个有 10 个元素的数组,除了前三个元素外其他元素都为 0
。
第二种变化:数组长度写成 ...
var arrLazy = [...]int{5, 6, 7, 8, 22}
...
同样可以忽略,从技术上说它们其实变成了切片。
第三种变化:key: value 语法
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串,所以输出结果为:
Person at 0 is
Person at 1 is
Person at 2 is
Person at 3 is Chris
Person at 4 is Ron
在这里数组长度同样可以写成 ...
。
// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"Tom" 6:"Dav" ]
var arr = [...]string{3: "Chris", 4: "Ron", "Tom", "Dav"}
// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"Tom" 6:"Dav" 7:"" 8:"" 9:"" ]
var arr = [10]string{3: "Chris", 4: "Ron", "Tom", "Dav"}
// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"" 6:"" 7:"" 8:"Tom" 9:"Dav" ]
var arr = [10]string{3: "Chris", "Ron", 8: "Tom", "Dav"}
2. 切片[2]
2.1 概念
切片 (slice) 是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。
切片是可索引的,并且可以由 len()
函数获取长度。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0, 最大为相关数组的长度:切片是一个 长度可变的数组。
切片提供了计算容量的函数 cap()
可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s
是一个切片,cap(s)
就是从 s[0]
到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于切片 s
来说该不等式永远成立:0 <= len(s) <= cap(s)
。
多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
优点:因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。
注意:绝对不要用指针指向切片。切片本身已经是一个引用类型,所以它本身就是一个指针!!
声明切片的格式是:
var identifier []type // 不需要说明长度
一个切片在未初始化之前默认为 nil
,长度为 0。
切片的初始化格式是:
// start <= end <= max <= len(arr1)
var slice1 []type = arr1[start:end:max]
这表示 slice1
是由数组 arr1
从 start
索引到 end-1
索引之间的元素构成的子集(切分数组,start:end:max
被称为切片表达式)。所以 slice1[0]
就等于 arr1[start]
。这可以在 arr1
被填充前就定义好。
如果某个人写:var slice1 []type = arr1[:]
那么 slice1
就等于完整的 arr1
数组(所以这种表示方式是 arr1[0:len(arr1)]
的一种缩写)。另外一种表述方式是:slice1 = &arr1
。
// 示例
var arr = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := arr[:] // [0 1 2 3 4 5 6 7 8 9]
s2 := arr[1:5] // [1 2 3 4]
s3 := arr[1:] // [1 2 3 4 5 6 7 8 9]
s4 := arr[5:] // [5 6 7 8 9]
s5 := arr[1:5:8] // [1 2 3 4]
注意:切片表达式中的 max
表示向 slice1
append 元素时使用的底层数组 arr1
对应的索引最大只能使用到 max-1
,超出 max-1
时 slice1
便会开辟一个新的底层数组并复制 arr1
中 start
到 max-1
的值到新数组中,并在新数组末尾继续 append 新的元素。例如:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6}
var s = arr[2:4:5] // [2 3]
fmt.Println(s, arr) // [2 3] [0 1 2 3 4 5 6]
s = append(s, 100) // 此时还共享 arr 地址
fmt.Println(s, arr) // [2 3 100] [0 1 2 3 100 5 6]
s = append(s, 200) // 此时已新开辟新的数组, 不再和 arr 共享地址
fmt.Println(s, arr) // [2 3 100 200] [0 1 2 3 100 5 6]
s[1] = 300 // 不再和 arr 共享地址
fmt.Println(s, arr) // [2 300 100 200] [0 1 2 3 100 5 6]
2.2 用 make() 创建一个切片
当相关数组还没有定义时,我们可以使用 make()
函数来创建一个切片,同时创建好相关数组:var slice1 []type = make([]type, len)
。
也可以简写为 slice1 := make([]type, len)
,这里 len
是数组的长度并且也是 slice
的初始长度。
所以定义 s2 := make([]int, 10)
,那么 cap(s2) == len(s2) == 10
。
make()
接受 2 个参数:元素的类型以及切片的元素个数。
如果你想创建一个 slice1
,它不占用整个数组,而只是占用 len
个项,那么只要:slice1 := make([]type, len, cap)
。
make()
的使用方式是:func make([]T, len, cap)
,其中 cap
是可选参数。
所以下面两种方法可以生成相同的切片:
make([]int, 50, 100)
new([100]int)[0:50]
2.3 new() 和 make() 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
new(T)
为每个新的类型T
分配一片内存,初始化为0
并且返回类型为*T
的内存地址:这种方法 返回一个指向类型为T
,值为0
的地址的指针,它适用于值类型如数组和结构体,相当于&T{}
。make(T)
返回一个类型为 T 的初始值,它 只适用 于 3 种内建的引用类型:切片
、map
和channel
。
换言之,new()
函数分配内存,make()
函数初始化。
new() 是一个函数,不要忘记它的括号。