Go语言 数组 切片 映射
Go语言数组、切片和映射
数组、切片、映射的内部实现和基础功能,创建、声明、使用、迭代、值传递
数组
Go语言的数组是一个长度固定的数据类型,用于储存一段具有相同类型元素的连续块,可以存放整数或字符型等内置类型,也可以是某种数据结构。
数组的内存是连续分配的。正因如此CPU能把正在使用的数据缓存更久的时间。也很容易计算索引,可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。
因为数组的每个元素类型相同,又是连续分配,就可以以固定速度索引数组中的任意数据,速度非常快。一旦声明,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
创建数组
声明变量时会使用对应类型的零值来对变量进行初始化
- 声明一个包含5个元素的整型数组
var array [5]int;
// [0 0 0 0 0]
- 声明数组时赋所有的值
数组字面量一种快速创建数组并初始化的方式,可以声明数组里元素的数量同时指定每个元素的值
// 用具体值初始化每个元素
array := [5]int{10, 20, 30, 40, 50}
//[10 20 30 40 50]
也可以使用...
替代数组的长度,就会根据初始化时数组元素的数量来确定该数组的长度
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}
- 声明时指定特定元素的值
// 用具体值初始化索引为1和2的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
使用数组
- 访问数组里某个单独元素,使用
[]
运算符
array := [5]int{10, 20, 30, 40, 50}
// 修改索引为2的元素的值
array[2] = 35
- 访问指针数组的元素
// 用整型指针初始化索引为0和1的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为0和1的元素赋值
*array[0] = 10
*array[1] = 20
- 同类型数组间赋值
数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。复制之后新数组就与原数组没有关系了,不会相互影响,这不切片不同。
// 声明第一个包含5个元素的字符串数组
var array1 [5]string
// 声明第二个包含5个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}// 把array2的值复制到array1
array1 = array2
复制后的两个数组的值完全一样
- 指针数组之间赋值
var array1 [3]*string
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将array2复制给array1
array1 = array2
多维数组
创建
根据创建数组的三种方式,同样可以创建多维数组
// 声明一个二维整型数组,两个维度分别存储4个元素和2个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为1个和3的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}
使用
只要类型(每一维度的长度以及最终存储在元素中的数据的类型)一致就可以将多维数组互相赋值
// 声明一个2×2的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40
// 将array2的值复制给array1
array1 = array2
- 使用索引为多维数组赋值
//将array1的索引为1的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为1、内层数组的索引为0的整型值复制到新的整型变量里
var value int = array1[1][0]
传递数组
- 值传递
// 声明一个需要8 MB的数组
var array [1e6]int
// 将数组传递给函数foo
foo(array)
// 函数foo接受一个100万个整型值的数组
func foo(array [1e6]int) {
...
}
每次函数foo被调用时,必须在栈上分配8 MB的内存。之后,整个数组的值(8 MB的内存)被复制到刚分配的内存里。
- 指针传递
// 分配一个需要8 MB的数组
var array [1e6]int
// 将数组的地址传递给函数foo
foo(&array)
// 函数foo接受一个指向100万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}
这样函数foo
接受一个指向100万个整型值的数组的指针。现在将数组的地址传入函数,只需要在栈上分配8字节的内存给指针就可以。
这个操作会更有效地利用内存,性能也更好。但因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。如果你不想被函数修改值,那就不要使用指针传递。
切片
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
结构
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有3个字段的数据结构,分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
对切片slice[i:j:k]
或底层数组容量是k
的切片slice[i:j]
来说
容量是该与切片相关联的所有元素的数量。
有两个特殊的内置函数len
和cap
,可以用于处理数组、切片和通道。对于切片,函数len
返回切片的长度,函数cap返回切片的容量。可以使用函数len
来决定什么时候停止对切片的迭代。
创建切片
- make
一种创建切片的方法是使用内置的make函数。当使用make时,需要传入一个参数,指定切片的长度。
// 创建长度和容量都是5个元素的字符串切片
slice := make([]string, 5)
如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量:
// 其长度为3个元素,容量为5个元素
slice := make([]int, 3, 5)
分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。
切片可以访问3个元素,而底层数组拥有5个元素。剩余的2个元素可以在后期操作中合并到切片,可以通过切片访问这些元素。如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。
- 使用切片字面量
// 创建其长度和容量都是5个元素的字符串切片
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
当使用切片字面量时,可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。
- 索引声明切片
// 创建长度和容量都是100个元素的字符串切片
// 使用空字符串初始化第100 个元素
slice := []string{99: ""}
数组与切片声明的不同
如果在[]
运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值的时候,才会创建切片
// 创建有3个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是3的整型切片
slice := []int{10, 20, 30}
nil和空切片
- nil切片
有时,程序可能需要声明一个值为nil的切片(也称nil切片)。只要在声明时不做任何初始化,就会创建一个nil切片
// 创建nil整型切片
var slice []int
nil切片是很常见的创建切片的方法。nil切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时,nil切片会很好用。例如,函数要求返回一个切片但是发生异常的时候
- 空切片
利用初始化,通过声明一个切片可以创建一个空切片
// 使用make创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}
空切片在底层数组包含0个元素,也没有分配任何存储空间。想表示空集合时空切片很有用,例如,数据库查询返回0个查询结果时
但不管是使用nil切片还是空切片,对其调用内置函数append、len和cap的效果都是一样的,因为他们仍然是个切片。
使用切片
赋值和切片
// 创建一个容量和长度都是5个元素的整型切片
slice := []int{10, 20, 30, 40, 50}
// 改变索引为1的元素的值
slice[1] = 25
切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分
slice := []int{10, 20, 30, 40, 50}
// 创建一个长度为2个元素,容量为4个元素的新切片
newSlice := slice[1:3]
执行完上述代码的切片动作后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分
第一个切片slice能够看到底层数组全部5个元素的容量,不过之后的newSlice就看不到。对于newSlice,底层数组的容量只有4个元素。newSlice无法访问到它所指向的底层数组的第一个元素之前的部分。所以,对newSlice来说,之前的那些元素就是不存在的。
需要注意的是,现在两个切片slice
和newSlice
共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。
func main() {
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片,长度是2个元素,容量是4个元素
newSlice := slice[1:3]
// 修改newSlice索引为1的元素
// 同时也修改了原来的slice的索引为2的元素
newSlice[1] = 35
fmt.Println(slice)
fmt.Println(newSlice)
}
//[10 20 35 40 50]
//[20 35]
把35赋值给newSlice的第二个元素(索引为1的元素)的同时也是在修改原来的slice 的第3个元素(索引为2的元素)
切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常runtime error: index out of range
。与切片的容量相关联的元素只能用于增长切片。在使用这部分元素前,必须将其合并到切片的长度里。
切片有额外的容量是很好,但是如果不能把这些容量合并到切片的长度里,这些容量就没有用处。
切片增长
append
函数会处理增加长度时的所有操作细节。
要使用append,需要一个被操作的切片和一个要追加的值。当append调用返回时,会返回一个包含修改结果的新切片。函数append总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为60
newSlice = append(newSlice, 60)
因为newSlice在底层数组里还有额外的容量可用,append操作将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的slice共享同一个底层数组,slice中索引为3的元素的值也被改动了。
- append同时增加切片的长度和容量
如果切片的底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值
slice := []int{10, 20, 30, 40}
// 向切片追加一个新元素
// 将新元素赋值为50
newSlice := append(slice, 50)
当这个append操作完成后,newSlice拥有一个全新的底层数组,这个数组的容量是原来的两倍
函数append会智能地处理底层数组的容量增长。在切片的容量小于1000个元素时,总是会成倍地增加容量。一旦元素个数超过1000,容量的增长因子会设为1.25,也就是会每次增加25%的容量。
内置函数append也是一个可变参数的函数。这意味着可以在一次调用传递多个追加的值。如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里。
func main() {
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
}
//[1 2 3 4]
切片的3个索引
在创建切片时,还可以使用之前我们没有提及的第三个索引选项。第三个索引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。可以看到,允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作。
// 创建长度和容量都是5个元素的字符串切片
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
- 3索引创建切片
// 将第三个元素切片,并限制容量
// 其长度为1个元素,容量为2个元素
slice := source[2:3:4]
这个切片操作执行后,新切片里从底层数组引用了1个元素,容量是2个元素。具体来说,新切片引用了Plum元素,并将容量扩展到Banana元素。
如果试图设置的容量比可用的容量还大,就会报错runtime error: slice bounds out of range
。内置函数append会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。
切片与原切片分离
这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改
func main() {
// 创建长度和容量都是5个元素的字符串切片
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 将第三个元素切片,并限制容量为1个元素
slice := source[2:3:3]
// 向slice追加新字符串
slice = append(slice, "Kiwi")
fmt.Println(source)
fmt.Println(slice)
}
//[Apple Orange Plum Banana Grape]
//[Plum Kiwi]
如果不加第三个索引,由于剩余的所有容量都属于slice,向slice追加Kiwi会改变原有底层数组索引为3的元素的值Banana。不过我们限制了slice的容量为1。当我们第一次对slice调用append的时候,会创建一个新的底层数组,这个数组包括2个元素,并将水果Plum复制进来,再追加新水果Kiwi,并返回一个引用了这个底层数组的新切片。
因为新的切片slice拥有了自己的底层数组,所以杜绝了可能发生的问题。我们可以继续向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。同时,也保持了为切片申请新的底层数组的简洁。
迭代切片
既然切片是一个集合,可以迭代其中的元素。Go语言有个特殊的关键字range,它可以配合关键字for来迭代切片里的元素,关键字range总是会从切片头部开始迭代。
当迭代切片时,关键字range会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。如果不需要索引值,可以使用占位字符_
来忽略这个值
func main() {
// 创建一个整型切片
// 其长度和容量都是4个元素
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
}
/*
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
*/
当迭代切片时,关键字range会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本
得到的元素是副本,不能使用该值变量的地址作为指向元素的指针。要想获取每个元素的地址,可以使用切片变量和索引值。
- 传统迭代法for
func main() {
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index :=2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
}
多维切片
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
我们有了一个包含两个元素的外层切片,每个元素包含一个内层的整型切片。
外层的切片包括两个元素,每个元素都是一个切片。第一个元素中的切片使用单个整数10来初始化,第二个元素中的切片包括两个整数,即100和200。
这种组合可以让用户创建非常复杂且强大的数据结构。已经学过的关于内置函数append 的规则也可以应用到组合后的切片上,
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为20的元素
slice[0] = append(slice[0], 20)
append函数处理追加的方式很简明:先增长切片,再将新的整型切片赋值给外层切片的第一个元素。当append操作完成后,会为新的整型切片分配新的底层数组,然后将切片复制到外层切片的索引为0的元素,如图所示。
传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。在64位架构的机器上,一个切片需要24字节的内存:指针字段需要8 字节,长度和容量字段分别需要8字节。
由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组
在函数间传递24字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。
func main() {
// 分配包含100万个整型值的切片
slice := make([]int, 1e6)
// 将slice传递到函数foo
slice = foo(slice)
// 函数foo接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}
}
映射
映射是一种数据结构,用于存储一系列无序的键值对。类似于python中的字典。映射里基于键来存储值。图 4-23通过一个例子展示了映射里键值对是如何存储的。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。
内部实现
映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的集合,意味着没有办法预测键值对被返回的顺序。即便使用同样的顺序保存键值对,每次迭代映射的时候顺序也可能不一样。无序的原因是映射的实现使用了散列表。
映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,所有操作都要先选择一个桶。把操作映射时指定的键传给映射的散列函数,就能选中对应的桶。这个散列函数的目的是生成一个索引,这个索引最终将键值对分布到所有可用的桶里。
键是字符串,代表颜色。这些字符串会转换为一个数值(散列值)。这个数值落在映射已有桶的序号范围内表示一个可以用于存储的桶的序号。之后,这个数值就被用于选择桶,用于存储或者查找指定的键值对。
映射使用两个数据结构来存储数据。第一个数据结构是一个数组,内部存储的是用于选择桶的散列键的高八位值。这个数组用于区分每个键值对要存在哪个桶里。第二个数据结构是一个字节数组,用于存储键值对。该字节数组先依次存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。实现这种键值对的存储方式目的在于减少每个桶所需的内存。
创建映射
- make函数
// 创建一个映射,键的类型是string,值的类型是int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是string
// 使用两个键值对初始化映射
dict:= map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
创建映射时,更常用的方法是使用映射字面量。映射的初始长度会根据初始化时指定的键值对的数量来确定。
映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==运算符做比较。切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误,如果使用字符串切片作为映射的键 dict := map[[]string]int{}
,便会编译错误Compiler Exception: invalid map key type []string
映射的值则可以使任何类型,没有任何理由阻止用户使用切片作为映射的值。这个在使用一个映射键对应一组数据时,会非常有用。
// 创建一个映射,使用字符串切片作为值
dict := map[int][]string{}
使用映射
键值对赋值给映射,是通过指定适当类型的键并给这个键赋一个值来完成的
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将Red的代码加入到映射
colors["Red"] = "#da1337"
- nil
可以通过声明一个未初始化的映射来创建一个值为nil的映射(称为nil映射)。
var colors map[string]string
nil映射不能用于存储键值对,否则,会产生一个语言运行时错误runtime error: assignment to entry in nil map
。
- 检查存在
测试映射里是否存在某个键是映射的一个重要操作。这个操作允许用户写一些逻辑来确定是否完成了某个操作或者是否在映射里缓存了特定数据。这个操作也可以用来比较两个映射,来确定哪些键值对互相匹配,哪些键值对不匹配。
同步判断:
func main() {
colors := map[string]string{}
colors["Red"] = "#da1337"
// 获取键Blue对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
}
}
值判断:
通过键来索引映射时,即便这个键不存在也总会返回一个值。在这种情况下,返回的是该值对应的类型的零值。注意,这种方法只能用在映射存储的值都是非零值的情况。
func main() {
colors := map[string]string{}
colors["Red"] = "#da1337"
// 获取键Blue对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}
}
- range迭代映射
func main() {
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
}
- delete删除
如果想把一个键值对从映射里删除,就使用内置的delete函数
// 删除键为Coral的键值对
delete(colors, "Coral")
传递映射
在函数间传递映射并不会制造出该映射的一个副本。当传递映射给一个函数,并对这个映射做了修改时,所有引用这个映射的地方都会察觉到这个修改,这个特性和切片类似,保证可以用很小的成本来复制映射。
func main() {
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("111Key: %s Value: %s\n", key, value)
}
// 调用函数来移除指定的键
removeColor(colors, "Coral")
for key, value := range colors {
fmt.Printf("222Key: %s Value: %s\n", key, value)
}
}
// removeColor将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
delete(colors, key)
}
/*
111Key: AliceBlue Value: #f0f8ff
111Key: Coral Value: #ff7F50
111Key: DarkGray Value: #a9a9a9
111Key: ForestGreen Value: #228b22
222Key: DarkGray Value: #a9a9a9
222Key: ForestGreen Value: #228b22
222Key: AliceBlue Value: #f0f8ff
*/
总结
-
数组是构造切片和映射的基石。
-
Go语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据。
-
内置函数make可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值。
-
切片有容量限制,不过可以使用内置的append函数扩展容量。
-
映射的增长没有容量或者任何限制。
-
内置函数len可以用来获取切片或者映射的长度。
-
内置函数cap只能用于切片。
-
通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。