数组与切片是Go语言中最常用的两个复合类型,它们代表的是一组连续存储同种类型元素的集合。除此之外,在Go语言中还有一种比较常用的复合类型 —— 可以让一个值唯一关联到一个特定的键上,用于实现特定键值的快速查找与更新 —— Map类型,也可以叫做映射、哈希或者字典。
Map 类型的特点
map 是 Go 语言提供的一种抽象数据类型,以 K-V 的形式展示,表示一组无序的键值对。而 map 类型中的每个 key 都是唯一的,map 类型写作:map[keyType]valueType。如果两个 map 类型的 key 元素类型相同且 value 元素类型也相同,我们可以认为它是同一个 map 类型。
在使用 map 类型时,我们需要时刻注意着,map 类型对 value的类型没有限制,但是对 key 的类型却有比较严格的要求,这是因为map类型要保证key的唯一性,而 Go 语言中也要求,key 的类型必须支持 "==" 和 "!=" 两种比较操作符。所以,map 的 key 类型是不支持函数类型、map类型自身以及切片的。
Map 类型的声明
map 类型支持以下几种声明方式:
- var nimMap map[keyType]valueType,映射的零值为 nil ,值为 nil 的映射长度为0 。如果尝试对 nil 映射写数据则会导致崩溃。
- 使用 := 并通过映射字面量创建,即 nMap := map[keyType]valueType{}。这种声明方式可以对空映射进行读写操作而不会出现崩溃的情况。
- 如果已知映射中需要多少镇值对,但不知确切的值 ,可以使用 map 来创建一个有默认长度的映射,即 cMap := make([keyType]valueType, 100),但是其长度依然为 0。不过,map 类型的容量不会受限于它的初始容量值,当键值对数量超过初始化容量后,Go 运行时会自动增加 map 类型的容量,以保证后续键值对的正常插入。
Map 类型的在哪些基本操作?
针对 map 类型变量,不外乎插入新键值对、获取当前变量的数量、查找特定键并读取对应值、删除、遍历等基础的操作。
插入新键值对
该操作需要符合两个特定条件:
- map 类型变量为非 nil值
- 插入的数据要符合 map 类型定义
也就是说,上面所有说三种声明 map 类型的方式,不支持第一种。
m := make(map[string]int)
m["key1"] = 1
m["key2"] = 2
m["key3"] = 3
在这里,我们不需要判断数据是否插入成功,Go运行时会负责 map 变量内部的内存管理,除非是系统内存耗尽,否则 Go 总会保证插入成功的。 另外,如果某个 key 已经存在于map中,我们再次插入则会将新值覆盖旧值。
获取 Map 类型键值对的数量
在实际业务开发中,我们经常会有获取 map 类型中有多少键值对的需求。与获取切片元素数量相似,可以同样使用 Go 语言中提供的内置函数 len 来获取已存储键值对的数量。不过需要注意的是, 虽然 Map 类型使用 make 声明时可以初始化容量,但是我们不能对 map 类型使用 cap 函数来获取当前的容量。
读取某个key的值
读取map类型中某个key的值,最常规的用法:
m := make(map[string]int)
v := m["key1"]
虽然用法无任何问题,但是我们需要考虑到某个key是否存在于map中。在 Go 语言中,如果要获取某个key不存在map中,是不会报错的,而是会获取到这个value类型的零值。所以,这里就会产生一些微妙的变化,如果值类型的 int 的话,会因为可能存在 0 而导致无法准确判断。 那么该如何判断key是否存在于map中呢?在 Go 语言中,map 类型支持使用"comma ok"的用法,即:
m := make(map[string]int)
v, ok := m["key1"]
if !ok {
// 说明 key1 不在Map中
}
所以, 除非你确定key一定存在于map中,否则我们应该使用 "comma ok" 的方式来判断及获取map的值。
删除数据
在 Go 语言中,删除 map 中的数据需要使用内置函数 delete,这也是从 map 中删除键的唯一方法。
m := map[string]int{
"apple": 10,
"bannar": 14
}
delete(m, "apple")
需要注意的是,即使传给delete的键在map中并不存在, delete 函数的执行也不会失败,也不会抛出运行时的异常。
map 的遍历
遍历map的键值对只能通过 for range 语句进行,与切片类似。
m := map[string]int{
"apple": 10,
"banana": 14,
"watermelon": 16
}
for k, v := range m {
fmt.Println(k, v)
}
// 如果只使用 key 的话
for k := range m{
}
// 如果只用到 value 的话
for _, v := range m {
}
其实遍历很好理解,但是在 Go 语言中,map类型有一个重要的特点:对同一个 map 做多次遍历时,每次遍历元素的次数都是不同的。所以,如果我们在业务中要尽量避免依赖遍历 map 的次序。 如果我们一定要给map进行某种规则排序遍历的话,理论上通过map是无法实现的,但是可以通过显性的给键进行排序。例如,键是字符串类型,可以通过 sort 包中的 Strings 函数来给键排序。 为什么会把map的遍历设置为随机?这个在Go 1.0 版本中可以看出一些端倪,我把原文贴出来:go.dev/doc/go1
Iterating in maps
The old language specification did not define the order of iteration for maps, and in practice it differed across hardware platforms. This caused tests that iterated over maps to be fragile and non-portable, with the unpleasant property that a test might always pass on one machine but break on another. In Go 1, the order in which elements are visited when iterating over a map using a for range statement is defined to be unpredictable, even if the same loop is run multiple times with the same map. Code should not assume that the elements are visited in any particular order. This change means that code that depends on iteration order is very likely to break early and be fixed long before it becomes a problem. Just as important, it allows the map implementation to ensure better map balancing even when programs are using range loops to select an element from a map. m := map[string]int{"Sunday": 0, "Monday": 1}
for name, value := range m {
// This loop should not assume Sunday will be visited first.
f(name, value)
} Updating: This is one change where tools cannot help. Most existing code will be unaffected, but some programs may break or misbehave; we recommend manual checking of all range statements over maps to verify they do not depend on iteration order. There were a few such examples in the standard repository; they have been fixed. Note that it was already incorrect to depend on the iteration order, which was unspecified. This change codifies the unpredictability.
Map 在函数间的传递
与切片类型相似,Map 也是引用类型,在函数间传递映射并不会复制底层的数据结构,其成本很小。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。 所以,我们也不用担心 map 在函数间传递的开销问题。