计算机科学中最实用的数据结构之一是哈希表。存在多种具有不同特性的哈希表实现,但总体而言,它们能实现快速的查找、添加和删除操作。Go 语言提供了一个内置的 map 类型,该类型正是基于哈希表实现的。
声明与初始化
Go map 类型声明如下:
map[KeyType]ValueTypeKeyType 必须是可比较类型 (comparable),ValueType 可以是任意类型(包括另一个 map)。
声明一个键为 string、值为 int 的 map:
var m map[string]intMap 是引用类型(类似指针或切片)。上述未初始化的 m 值为 nil。读取 nil map 表现如空 map,但写入 nil map 会导致运行时 panic。
使用内置的 make 函数初始化 map:
m = make(map[string]int)make 函数分配并初始化哈希映射数据结构,返回指向它的 map 值。具体数据结构是运行时的实现细节。
使用 map
m["route"] = 66i := m["route"]如果请求的键不存在,将返回值类型的零值。这里值类型是 int,所以零值为 0:
j := m["root"]
// j == 0n := len(m)使用内置的 delete 函数从 map 中删除条目(如果键不存在则无操作,且无返回值):
delete(m, "route")使用双值赋值测试键是否存在:
i, ok := m["route"]如果键 "route" 存在,i 为对应值,ok 为 true。如果不存在,i 为零值(0),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 可用作类似集合 (set) 的数据结构(布尔类型的零值为 false)。以下示例遍历 Node 链表并打印其值,使用 map[*Node]bool 检测链表中的环:
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。向 nil 切片追加元素会自动分配新切片,因此向切片 map 追加值只需一行代码,无需检查键是否存在。
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)
}
}打印喜欢 cheese 的人:
for _, p := range likes["cheese"] {
fmt.Println(p.Name, "likes cheese.")
}打印喜欢 bacon 的人数:
fmt.Println(len(likes["bacon"]), "people like bacon.")由于 range 和 len 都将 nil 切片视为长度为零的切片,即使没人喜欢 cheese 或 bacon,上述代码也能正常运行。
键类型
map 的键可以是任何可比较 (comparable) 的类型。包括:布尔值、数字、字符串、指针、通道、接口类型,以及仅包含这些类型的结构体或数组。
切片、map 和函数不可比较(不能使用 ==),因此不能用作 map 键。
结构体可用作键以实现多维数据索引。例如,统计各国对网页的访问量,可以使用 map 嵌套:
hits := make(map[string]map[string]int)获取澳大利亚人加载文档页面的次数:
n := hits["/doc/"]["au"]这种方法在添加数据时很繁琐,必须检查内部 map 是否存在并在需要时创建它:
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 的行为是未定义的。如果需要从并发执行的 goroutine 中读写 map,必须使用同步机制(如 sync.RWMutex)进行协调。
声明一个包含 map 和嵌入 sync.RWMutex 的匿名结构体变量:
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}读取时获取读锁:
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)写入时获取写锁:
counter.Lock()
counter.m["some_key"]++
counter.Unlock()迭代顺序
使用 range 循环遍历 map 时,迭代顺序是未指定的,且不保证每次迭代顺序相同。如果需要稳定的迭代顺序,必须维护一个单独的数据结构来指定该顺序。
以下示例使用一个单独的排序键切片,按键顺序打印 map[int]string:
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])
}