通向Golang的捷径【8. map】

map 是一种特殊的数据结构, 也是一组元素对的无序集合, 元素对的一个元素被称为 key(键值), 另一个元素是与 key 关联的 value(数值或数据), 因此这类数据结构被称为关联数组或字典, 它可实现 value 的快速检索,即给出一个 key, 可快速获取到与 key 对应的 value.

在大多数编程语言中, 也提供了类似的数据结构, 比如字典类型 (Python 语言为 dict 类型), 哈希类型, 哈希表等.

8.1 声明, 初始化和创建

8.1.1 概念

map 是一种引用类型, 并可使用以下格式进行声明:
在这里插入图片描述
在 [key 类型]value 类型之间允许出现空格, 但 gofmt 工具会将这些空格移除. 由于 map 声明中无法设定 map的长度, 因此 map 可实现动态调整.

key 类型可为任意类型 (并可使用 == 或!= 操作符进行比较), 比如 string,int, float, 而 key 类型不能使用数组,slice 和结构类型, 但指针类型和接口类型可以使用, 同时也可使用结构类型生成一个 key(类似于 Key() 或Hash() 方法所生成的 key), 即基于结构类型的数据域, 可计算出一个 key(它将是一个唯一的数值或是一个字串 key).

value 类型也可以是任意类型, 并且还可使用空接口类型 (参见 11.9 节), 同时 value 中也可存储任意数值, 只是在使用 value 之前, 必须首先进行类型断言 (检查), 参见 11.3 节.

向函数传递 map 的成本更低, 在 32 位系统中只需 4 个字节, 在 64 位系统中只需 8 个字节, 并且无须保存其他数据, 使用 key, 查找 value(map 中) 的速度最快, 而且比线性搜索更快, 但是相比于数组或 slice 的索引值搜索则更慢, 通常在百倍以上, 所以在要求性能的场合中, 推荐使用 slice.

map 提供的 value 可实现分支结构 (参见第 5 章), 即使用 key, 选择不同的逻辑分支, 以执行不同的函数, 如果map1 包含了 key1,map1[key1] 可获取与 key1 对应的 value, 这与数组的索引值很相似, 因此数组可视为 map的简单形式, 它的 key(索引值) 都是整型, 并从 0 开始. 与 key1 关联 value 值, 可使用赋值实现修改:
在这里插入图片描述
同时与 key1 关联的 value 值, 也可保存在其他变量 v 中, 如果 map 未包含 key1, v 将包含 value 类型的默认值.
在这里插入图片描述
len(map1) 可获得 map 中元素对的个数, 在执行过程中, 元素对可实现动态添加和删除, 因此其个数也会产生变化.

例 8.1 make_maps.go

在这里插入图片描述
在这里插入图片描述
mapLite 使用了 map 的通用声明方式, 并包含了初始化数据 {key1: val1, key2: val2}, 这类方式与数组和结构很相似.mapAssigned 引用了 mapLit, 因此修改 mapAssigned 也将造成 mapLit 的变更. 使用 make 函数可实现 map 类型的内存分配:
在这里插入图片描述
注意: 不要使用 new 函数, 而应当使用 make 函数创建 map. 如果使用 new 函数, 将返回一个为 nil 的指针, 这等同于声明了一个未初始化的变量, 并返回了该变量的地址, 如下:
在这里插入图片描述
为证明 value 可为任意类型, 以下 map 示例中, 会将 value 类型设定为 func() int:

例 8.2 map_func.go

在这里插入图片描述
在这里插入图片描述
map 将包含 1:0x10903be0, 2:0x10903bc0, 5:0x10903ba0 元素对, 整型 key 将与函数的入口地址建立关联.

8.1.2 长度

与数组不同,map 可动态添加新的键值对 (key-value), 因此 map 不会给出一个固定尺寸或是最大尺寸, 但是map 可选择一个初始长度, 比如:
在这里插入图片描述
在 map 中添加了一个新的键值对后,map 长度将自动加 1, 如果需要在大型 map 中添加键值对, 或是需要在map 中添加大量键值对时, 设定一个初始长度, 可提升添加操作的性能, 这是经过验证基本可确定的结论.

在以下 map 中, 包含了音符名称及其对应的频率值, 频率值的单位为 Hz, 比如 A4=440Hz,
在这里插入图片描述

8.1.3 在 value 中使用 slice

一个 key 只能关联一个 value, 同时 value 可使用一个基本类型, 如果一个 key 需要关联多个 value 时, 比如在Unix 系统的进程管理, 可将一个父进程视为 key(进程 id, 即 pid, 它是一个 int 类型), 而一个父进程可包含多个子进程 (可将这些子进程 id 组合成一个 slice), 因此将 value 类型指定为 []int 或是其他类型的 slice, 可解决上述问题, 如下:
在这里插入图片描述

8.2 map 测试与 map 元素的删除

val1 = map1[key1] 可返回与 key1 关联的 value 值 val1, 如果 key1 不存在,val1 将获得 value 类型的默认值.这里出现了一个歧义, 即 key1 存在, 但 value 却给出了类型的默认值. 为了测试 key1 是否存在, 可使用以下语句:
在这里插入图片描述
isPresent 可返回一个布尔值, 如果 key1 存在,val1 将获取 key1 的 value, 且 isPresent 为 true, 如果 key1 不存在,val1 将获得 value 类型的默认值, 且 isPresent 为 false. 如果只需测试 key 是否存在, 而无须获取 value 时,可使用:
在这里插入图片描述
同时可在 if 语句中, 提供以下的组合语句:
在这里插入图片描述
从 map1 中删除 key1 元素对, 可使用以下语句:
在这里插入图片描述
如果 key1 不存在, 上述语句也不会产生一个错误.

例 8.3 map_testelement.go

在这里插入图片描述
在这里插入图片描述

8.3 for-range

在 map 中可使用以下控制结构:
在这里插入图片描述
key 和 value 即为 map 的 key 和 value, 在 for-range 程序块中, 它们将被视为局部变量, 同时 map 元素对的迭代将是随机选择, 如果只需要 value 值, 可使用:
在这里插入图片描述
如果只需要 key, 可使用
在这里插入图片描述

例 8.4 maps_forrange.go

在这里插入图片描述
从上可见 map 的输出, 既没有按 key 值排序, 也没有按 value 值排序.

8.4 基于 map 的 slice

如果需要创建一个由 map 组合的 slice, 则必须调用 make() 两次, 第一次用于创建 slice, 之后用于创建每个map, 如下例. 通过 slice 的索引值, 可访问每个 map 元素, 这是版本 A 的思路, 而在版本 B 中,map 元素只是一个副本, 因此 map 变量并未进行初始化.

例 8.5 slice_maps.go

在这里插入图片描述
在这里插入图片描述

8.5 排序

默认情况下,map 不会进行排序, 无论是基于 key 或是 value, 如果需要对 map 进行排序, 应将 key(或 value)复制到一个 slice 中, 首先实现该 slice 的排序 (使用 sort 包), 之后在 for-range 中, 基于 slice 的元素, 搜索对应 map 内容, 实现 map 的有序输出, 如下:

例 8.6 sort_map.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用结构 slice 也可实现一个有序列表, 同时效率更高:
在这里插入图片描述

8.6 转换

如果 map 的 key 和 value 需要转换, 首先 value 类型与 key 类型必须兼容, 同时 value 值也存在唯一性, 那么就可实现相互转换:

例 8.7 invert_map.go

在这里插入图片描述
在这里插入图片描述
如果 value 值并不存在唯一性, 那么转换将出现错误, 但是在上述代码中, 并不会给出一个错误, 如果遇到一个非唯一的 key, 转换操作将停止, 因此可能会出现, 新 map 无法完全包含原有 map 的所有键值对, 对应的解决方案是, 测试原有 map 中所有 vale 值的唯一性.

在这里插入图片描述