Go 博客
Go map in action
引言
计算机科学中最有用的数据结构之一是哈希表。存在许多具有不同特性的哈希表实现,但总的来说,它们提供了快速的查找、添加和删除。Go 提供了一个内置的 map 类型,它实现了哈希表。
声明和初始化
Go 的 map 类型如下所示
map[KeyType]ValueType
其中 KeyType
可以是任何 可比较 的类型(稍后详细介绍),而 ValueType
可以是任何类型,包括另一个 map!
变量 m
是一个从 string 键映射到 int 值的 map
var m map[string]int
Map 类型是引用类型,类似于指针或 slice,因此上面 m
的值为 nil
;它不指向已初始化的 map。nil map 在读取时表现得像一个空 map,但尝试写入 nil map 会导致运行时 panic;请勿这样做。要初始化 map,请使用内置的 make
函数
m = make(map[string]int)
make
函数会分配并初始化一个哈希 map 数据结构,并返回指向它的 map 值。该数据结构的具体实现是运行时的一个实现细节,语言本身并未指定。在本文中,我们将重点关注 map 的使用,而不是它们的实现。
使用 map
Go 提供了熟悉的 map 操作语法。此语句将键 "route"
设置为值 66
m["route"] = 66
此语句检索键 "route"
下存储的值,并将其赋给新变量 i
i := m["route"]
如果请求的键不存在,我们将得到值类型的零值。在本例中,值类型是 int
,因此零值是 0
j := m["root"]
// j == 0
内置的 len
函数返回 map 中条目的数量
n := len(m)
内置的 delete
函数会从 map 中删除一个条目
delete(m, "route")
delete
函数不返回任何内容,如果指定的键不存在,则什么也不做。
一个双值赋值用于测试键是否存在
i, ok := m["route"]
在此语句中,第一个值(i
)被赋给键 "route"
下存储的值。如果该键不存在,i
将是值类型的零值(0
)。第二个值(ok
)是一个 bool
,如果键存在于 map 中,则为 true
,否则为 false
。
要测试键是否存在而不检索值,请在第一个值的位置使用下划线 (_)
_, ok := m["route"]
要迭代 map 的内容,请使用 range
关键字
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
要用一些数据初始化 map,请使用 map 字面量
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
相同的语法可用于初始化空 map,这在功能上与使用 make
函数相同
m = map[string]int{}
利用零值
当键不存在时,map 检索返回零值可能很方便。
例如,布尔值 map 可以用作类似集合的数据结构(回想一下布尔类型的零值是 false)。此示例遍历 Node
的链表并打印其值。它使用 Node
指针的 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) }
表达式 visited[n]
如果 n
已被访问,则为 true
,如果 n
不存在,则为 false
。无需使用双值形式来测试 n
在 map 中是否存在;零值默认值可以帮我们完成。
另一个有用的零值示例是 slice 的 map。追加到 nil slice 会分配一个新的 slice,因此将值追加到 slice 的 map 只需要一行代码;无需检查键是否存在。在以下示例中,slice people 被 Person
值填充。每个 Person
都有一个 Name
和一个 Likes slice。该示例创建了一个 map,将每个 like 与喜欢它的人的 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 都将 nil slice 视为零长度 slice,因此最后两个示例即使没有人喜欢奶酪或培根(尽管这可能不太可能)也能正常工作。
键类型
如前所述,map 键可以是任何可比较的类型。 语言规范精确地定义了这一点,但简而言之,可比较的类型是布尔型、数值型、字符串型、指针型、通道型和接口型,以及仅包含这些类型的结构体或数组。列表中值得注意的是 slice、map 和函数;这些类型不能使用 ==
进行比较,也不能用作 map 键。
显而易见,字符串、整数和其他基本类型应该可以作为 map 键,但可能出乎意料的是结构体键。结构体可用于按多个维度对数据进行键控。例如,这个 map 的 map 可用于按国家统计网页访问量
hits := make(map[string]map[string]int)
这是从 string 到(从 string
到 int
的 map)的 map。外层 map 的每个键都是一个网页的路径,其中包含自己的内层 map。每个内层 map 的键是一个两位数的国家代码。此表达式检索澳大利亚人加载文档页面的次数
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 不适用于并发访问:同时读写它们会发生什么是不确定的。如果您需要从并发执行的 goroutine 中读取和写入 map,则必须通过某种同步机制来协调访问。保护 map 的一种常见方法是使用 sync.RWMutex。
此语句声明了一个 counter
变量,它是一个匿名结构体,包含一个 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 时,迭代顺序未指定,并且不能保证每次迭代都相同。如果您需要稳定的迭代顺序,则必须维护一个单独的数据结构来指定该顺序。此示例使用一个单独的已排序键 slice 来按键顺序打印 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])
}
下一篇文章:去 Go 会议
上一篇文章:格式化你的 Go 代码
博客索引