Go maps in action

计算机科学中最实用的数据结构之一是哈希表。存在多种具有不同特性的哈希表实现,但总体而言,它们能实现快速的查找、添加和删除操作。Go 语言提供了一个内置的 map 类型,该类型正是基于哈希表实现的。

声明与初始化

Go map 类型声明如下:

map[KeyType]ValueType

KeyType 必须是可比较类型 (comparable),ValueType 可以是任意类型(包括另一个 map)。

声明一个键为 string、值为 int 的 map:

var m map[string]int

Map 是引用类型(类似指针或切片)。上述未初始化的 m 值为 nil。读取 nil map 表现如空 map,但写入 nil map 会导致运行时 panic。

使用内置的 make 函数初始化 map:

m = make(map[string]int)

make 函数分配并初始化哈希映射数据结构,返回指向它的 map 值。具体数据结构是运行时的实现细节。

使用 map

m["route"] = 66
i := m["route"]

如果请求的键不存在,将返回值类型的零值。这里值类型是 int,所以零值为 0

j := m["root"]
// j == 0
n := len(m)

使用内置的 delete 函数从 map 中删除条目(如果键不存在则无操作,且无返回值):

delete(m, "route")

使用双值赋值测试键是否存在:

i, ok := m["route"]

如果键 "route" 存在,i 为对应值,oktrue。如果不存在,i 为零值(0),okfalse。若只需测试键是否存在而不获取值,可使用下划线 _ 忽略第一个值:

_, 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.")

由于 rangelen 都将 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])
}