引言
哈希表是计算机科学中最有用的数据结构之一。当前许多哈希表的实现,虽然有各自不同的特性,但它们都提供快速的查找、添加、及删除操作。Go 提供了内置的哈希表实现类型,即 map 类型。
声明及初始化
一个 Go 的 map 类型看起来是下面这个样子的:
map[KeyType]ValueType
其中,KeyType 可以是任意可比较的 (comparable, 稍后会介绍) 类型,ValueType 可以是任意类型,包括另外一个 map!
下面的变量 m 表示字符串的键映射到 int 值的 map:
var m map[string]int
Map 类型是引用类型,像指针或切片 (slice) 一样,因此上面 m 的值是 nil;它不指向一个完成初始化的 map。对于一个nil map,从它读取数据的时候,等价于从一个 (初始化好的) 空 map 读取数据;但向它写入数据时,会报运行期错误 (runtime panic),这是需要避免的操作。初始化 map 需要使用内置的 make 函数:
m = make(map[string]int)
make 函数分配内存地址,并在这个地址上初始化一个哈希 map 的数据结构,然后返回指向这个数据结构内存地址的 map 值。这个数据结构的细节是运行期的实现细节,并不是由语言自己确定的。在本文中,我们着重考虑 map 的使用,而不是实现。
操作 maps
Go 操作 map 的语法很简单熟悉。下面操作设置键 “route” 映射到值 66 上:
m["route"] = 66
下面的操作从 map 中获取键为 “route” 对应的值,并把这个值赋给新的变量 i:
i := m["route"]
如果请求的键不存在,那么我们会得到值相应类型的零值。在这个例子里,值的类型是 int,那么零值就是 0:
j := m["root"]
// j == 0
内置的 len 函数返回 map 中键值对的数量:
n := len(m)
内置的 delete 函数删除 map 中的键值对:
delete(m, "route")
delete 函数不会返回任何值。如果要删除的键值对不存在,那么 delete 函数不会做任何事情。
二值赋值操作用于检测 key 是否存在:
i, ok := m["route"]
在这个操作中,i 表示键 “route” 对应的值。如果这个键不存在,那么 i 为 0。ok 是一个 bool 类型的值,表示键是否存在;如果键存在,那么 ok 为 true;如果键不存在,那么 ok 为 false。
如果只想测试键值对不存在,而不获取相应的值,那么可以在第一个返回值处用下划线表示:
_, ok := m["route"]
可以使用 range 关键字来遍历 map 的内容:
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
使用已有数据初始化 map 时,使用以下语法:
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
也可以用这个语法来初始化空的 map,等价于使用 make 函数:
m = map[string]int{}
利用零值
当键值对不存在时,从 map 读取相应的键,返回零值。这个特性在实际应用中会非常方便:
例如:值为 boolean 类型的 map 可以用来当做类似 set 的数据结构 (boolean 的零值是 false)。下面的例子遍历一个链表,并打印各节点的值。这个程序使用节点的 map 来检测链表中是否有环。
type Node struct {
Next *Node
Value interface{}
}
var first *Node
visited := make(map[*Node]bool)
for n := first; n != nil; n = n.Next {
if visited[n] {
fmt.Println("cycle detected")
break
}
visited[n] = true
fmt.Println(n.Value)
}
如果节点 n 已经被访问过了,那么 visited[n] 返回 true,否则返回 false。在这里我们没必要使用二值类型来检测 n 是否存在于 map 中,而是使用默认的零值来完成相应的功能。
另一个使用零值的例子是 slice 的 map。向一个 nil 的 slice 添加值,会分配一个新的 slice,向 map 里的 slice 添加值也是一样的;我们没必要去检查 slice 是否存在。在下面这个例子中,people 这个 slice 存储的是 Person 类型的值。每个 Person 有一个 Name 和一个 slice 类型的 Likes。下面的例子创建了一个 map 来关联每个爱好和一个存放拥有这个爱好的人的 slice。
type Person struct {
Name string
Likes [] string
}
var people []*Person
likes := make(map[string][]*Person)
for _, p := range people {
for _, l := range p.Likes {
likes[l] = append(likes[l], p)
}
}
下面的代码打印所有喜欢奶酪的人:
for _, p := range likes["cheese"] {
fmt.Println(p.Name, "likes cheese.")
}
下面的代码打印喜欢培根的人的数量:
fmt.Println(len(likes["bacon"]), "people like bacon.")
注意,由于 range 和 len 会把 ni 类型的 slice 当做零长度的 slice 来处理,所以上面两个例子在每人喜欢奶酪或培根的情况下也能正确执行 (但是那不太可能发生 - 作者意思是人人都喜欢奶酪和培根 译者注)。
键的类型
如之前所提,map 的键的类型可以是任意可比较的 (comparable) 类型。Go 语言规范里面对此有详细定义。简而言之,可比较类型有 boolean、numberic、string、指针、channel、以及 interfece,还有只含有以上类型的结构体或数组。值得注意的是,不可比较的类型包括 slice、map、和函数。这些类型不能使用 == 来比较,也不能用作 map 的键。
很明显字符串、整型值、以及其他基础数据类型可以用作 map 的键,但是比较意外的是,结构体也可以作为 map 的键。结构体可以在多维中被使用。例如,下面一个 map 用作匹配与各个国家相符的网页:
hits := make(map[string]map[string]int)
这个 map 从字符串映射到另一个 map (在这个 map 里从字符串映射到整型值)。对于外部的 map,每个 key 表示一个网页的路径。对于内部的 map,每个 key 是一个二个字符的国家码。下面的命令返回澳大利亚人访问文档页的次数:
n := hits["/doc/"]["au"]
当添加数据时,这种方式会显得很笨重,因为对于每个给定的外部键,你需要检查内部键是否存在,并在需要的时候创建它:
func add(m map[string]map[string]int, path, country string) {
mm, ok := m[path]
if !ok {
mm = make(map[string]int)
m[path] = mm
}
mm[country]++
}
add(hits, "/doc/", "au")
下面的设计能简化上面的复杂操作,它使用一个简单的 map,键类型是一个结构体:
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
当一个越南人访问主页时,可以使用以下操作累加:
hits[Key{"/", "vn"}]++
读取操作也很直观。比如看有多少瑞士人看了规范页面:
n := hits[Key{"/ref/spec", "ch"}]
并发
Map 并不是并发安全的:并没有定义当你并发读写一个 map 时,到底会发生什么。如果你需要在并发的协程中分别读写一个 map,那么对 map 的访问必须使用并发机制来控制。一个常用的保护 map 的方法是使用 sync.RWMutex
下面的例子生命了一个名叫 counter 的变量,类型是一个匿名结构体,包含一个 map 和一个内置的 sync.RWMutex。
var counter = struct {
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
从 counter 读取 map 前,需要先获取读锁:
counter.RLock()
n := counter.m["some_key"]
counter.RUnlokc()
fmt.Println("some_key:", n)
往 counter 的 map 写入之前,需要先获取写锁:
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
迭代顺序
使用 range 来遍历 map 时,迭代顺序是不确定的。每次迭代的顺序也不保证一样。自从 Go 1 以来,运行期会随机化 map 的迭代顺序,因为码农们会依赖先前实现的稳定迭代顺序。如果你需要稳定的迭代顺序,你必须适应一个额外的数据结构来指明顺序。下面的例子使用一个额外的键的有序切片来按照键的顺序打印 map:
import "sort"
var m map[int]string
var keys []int
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
-- 作者:Andrew Gerrand