数组、切片和映射-《Go In Action》-Ch4

数组、切片和映射-《Go In Action》-Ch4

4.1 数组的内部实现和基础功能

4.1.1 内部实现

长度固定、内存连续分配、CPU数据缓存更久、容易计算索引,迭代速度快

4.2.1 声明和初始化

声明需要类型和长度,长度一旦确定就不能改变

1
var array [5] int

声明变量时,会使用对应类型的零值对变量进行初始化
可以使用字面变量声明数组

1
array := [5]int{10, 20, 30, 40, 50}

也可以使用…替代数组长度,Go会根据初始化时数组元素的数量来确定数组的长度

1
array := [...]int{10, 20, 30, 40, 50}

还可以给指定位置赋值确定值

1
array := [5]int{1: 10, 2: 20}

4.1.3 使用数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
array := [5]int{10, 20, 30, 40, 50}
// 修改索引为2 的元素的值
array[2] = 35

// 声明包含5 个元素的指向整数的数组
// 用整型指针初始化索引为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

// 声明第一个包含4 个元素的字符串数组
var array1 [4]string
// 声明第二个包含5 个元素的字符串数组
// 使用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 将array2 复制给array1
array1 = array2
// Compiler Error:
// cannot use array2 (type [5]string) as type [4]string in assignment

// 声明第一个包含3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将array2 复制给array1
array1 = array2

4.1.4 多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 声明一个二维整型数组,两个维度分别存储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

// 声明两个不同的二维整型数组
var array1 [2][2]int
var array2 [2][2]int
// 为每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将array2 的值复制给array1
array1 = array2

// 将 array1 的索引为1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为1、内层数组的索引为0 的整型值复制到新的整型变量里
var value int = array1[1][0]

4.1.5 在函数间传递数组

内存和性能上,传递数组是个很大的开销,因为总是值传递,需要拷贝,可以使用指针在函数间传递大数组,但是传递指针,函数会有改变指针指向的值的权限

1
2
3
4
5
6
7
var array [1e6]int
// 将数组的地址传递给函数foo
foo(&array)
// 函数foo 接受一个指向100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}

4.2 切片的内部实现和基础功能

切片类似于动态数组,可以按需自动增长和缩小,通过内置append函数,可以高效增长切片,切片在内存中连续分配,可以索引、迭代

4.2.1 内部实现

三个要素:指向底层数组的指针、切片访问元素的个数(即长度)和切片允许增长到的元素个数(即容量)

4.2.2 创建和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 创建一个字符串切片
// 其长度和容量都是5 个元素
slice := make([]string, 5)
// 创建一个整型切片
// 分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素 // 这里不能访问最后两个元素
slice := make([]int, 3, 5)

// 容量小于长度的切片会在编译时报错
// 创建一个整型切片
// 使其长度大于容量
slice := make([]int, 5, 3)
// Compiler Error:
// len larger than cap in make([]int)

// 通过切片字面量来声明切片
// 创建字符串切片
// 其长度和容量都是5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个整型切片
// 其长度和容量都是3 个元素
slice := []int{10, 20, 30}

// 使用索引声明切片
// 创建字符串切片
// 使用空字符串初始化第100 个元素
slice := []string{99: ""}

// 创建nil 整型切片
// 数组指针为nil,长度和容量都是0
var slice []int

// 声明空切片
// 数组包含0个元素,长度和容量都是0
// 使用make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}

4.2.3 使用切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 创建一个整型切片
// 其容量和长度都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 改变索引为1 的元素的值
slice[1] = 25

// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]

// 修改切片内容可能导致的结果
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度是2 个元素,容量是4 个元素
newSlice := slice[1:3]
// 修改newSlice 索引为1 的元素
// 同时也修改了原来的slice 的索引为2 的元素
newSlice[1] = 35

// 表示索引越界的语言运行时错误
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]
// 修改newSlice 索引为3 的元素
// 这个元素对于newSlice 来说并不存在
newSlice[3] = 45
// Runtime Exception:
// panic: runtime error: index out of range

// 切片增长
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为60
// 注意此时 slice变为:{10, 20, 30, 60, 50}
// append时如果容量有剩余,会在现有数组上增加元素,如果容量没有剩余,会创建一个新的数组,并想现有值复制到新的数组上
// 当切片容量小于1000时,每次扩展成倍增加,一旦元素超过1000,容量银子会设为1.25,也就是每次增加25%
newSlice = append(newSlice, 60)

// 创建切片时的3个索引
// 创建字符串切片
// 其长度和容量都是5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 将第三个元素切片,并限制容量
// 其长度为1 个元素,容量为2 个元素
slice := source[2:3:4]
// 这比可用的容量大
slice := source[2:3:6]
// Runtime Error:
// panic: runtime error: slice bounds out of range

// 3个索引一旦长度和容量设置的不一样,新的切片和原始切片公用相同的底层数组,对新切片的append会影响到原始切片,很容发生莫名其妙的问题,
// 此时可将新切片的容量设置为和长度一样,再执行append的时候,就会创建新的底层数组,从而和原始切片脱离关系,可以放心修改
// 创建字符串切片
// 其长度和容量都是5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 对第三个元素做切片,并限制容量
// 其长度和容量都是1 个元素
slice := source[2:3:3]
// 向slice 追加新字符串
slice = append(slice, "Kiwi")

// 将一个切片追加到另一个切片
// ...运算符,可以将一个切片的所有元素追加到另一个切片里
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
Output:
[1 2 3 4]

// 迭代切片for range
// 关键字range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本
// 需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用
// 创建一个整型切片
// 其长度和容量都是4 个元素
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
// Output:
// Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
// Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
// Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
// Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

4.2.4 多维切片

1
2
3
4
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为20 的元素
slice[0] = append(slice[0], 20)

4.2.5 在函数间传递切片

1
2
3
4
5
6
7
8
9
10
// 成本很低,在 64 位架构的机器上,一个切片需要24 字节的内存:指针字段需要8 字节,长度和容量
字段分别需要8 字节。
slice := make([]int, 1e6)
// 将slice 传递到函数foo
slice = foo(slice)
// 函数foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}

4.3 映射的内部实现和基础功能

4.3.1 内部实现

桶 + 两个数组
key转换成散列值,散列低位表示桶的序号,每个桶内有两个数组构成,第一个数组存储散列键的高发位置,第二个数组是一个字节数组,用于存储键值对,该字节数组先依次存储了这个桶里的所有键,
之后依次存储了这个桶里的所有值。

映射的内部结构的简单表示

4.3.2 创建和初始化

映射的键可以是任何可以使用(==)比较的值,切片、函数以及包含切片的结构类型不能作为映射的键,因为他们包含引用语义

1
2
3
4
5
6
7
8
9
10
// 创建一个映射,键的类型是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

4.3.3 使用映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"

// 通过声明映射创建一个nil 映射, 可以通过声明一个未初始化的映射来创建一个值为nil 的映射(称为nil 映射)。nil 映射
// 不能用于存储键值对,否则,会产生一个语言运行时错误
var colors map[string]string
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"
// Runtime Error:
// panic: runtime error: assignment to entry in nil map

// 从映射获取值并判断键是否存在
// 获取键Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
}

// 从映射获取值,并通过该值是否为零值来判断键是否存在
// 获取键Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}

// 遍历for range
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
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)
}

// 删除键为Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}

4.3.4 在函数间传递映射

在函数间传递映射并不会制造出该映射的副本,实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改,和切片类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
}
// 调用函数来移除指定的键
removeColor(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
}
// removeColor 将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
delete(colors, key)
}

// output
// Key: AliceBlue Value: #F0F8FF
// Key: Coral Value: #FF7F50
// Key: DarkGray Value: #A9A9A9
// Key: ForestGreen Value: #228B22
// Key: AliceBlue Value: #F0F8FF
// Key: DarkGray Value: #A9A9A9
// Key: ForestGreen Value: #228B22

4.4 小结

数组是构造切片和映射的基石
Go语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据
内置函数make可以创建切片和映射,并指定原始的长度和容量,也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值
切片有容量限制,不过可以使用内置的append函数扩展容量
映射的增长没有容量或者任何限制
内置函数len可以用来获取切片或者映射的长度
内置函数cap只能用于切片
通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值,但是切片不能用作映射的键
将切片或者映射传递给函数的成本很小,并且不会复制底层的数据结构

# Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×