Go map 实战教程
Andrew Gerrand
2013年2月6日
简介
hash map 是计算机科学领域中最常用的数据结构之一。 许多 hash map 实现具有各种各样的特性, 但通常他们都有快速查找、添加和删除元素的功能. Go 提供了内置的 hash map.
声明和初始化
Go 的map
类型长这样:
map[KeyType]ValueType
它的 KeyType
可以是任意可比较的 类型(稍后会进行详述),而 ValueType
可以是任意类型, 甚至可以是另一个 map
!
变量 m
是一个map
,它的键是 string 类型,值是 int 类型:
var m map[string]int
map 类型是引用类型, 就像指针或者切片, 因此上面的 m
的值是 nil
; 它没有指向一个已初始化的 map. 读取一个值为 nil 的 map 就像读取一个空的 map 一样, 但如果尝试向一个值为 nil 的 map 写入数据时,就会产生运行时 panic; 请不要这么做。 可以使用内置的 make
函数去初始化一个 map:
m = make(map[string]int)
make
函数为 hash 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"]
使用 range
关键字去迭代 map 的内容:
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检索会产生零值,这有时是很方便的。
例如,布尔值映射可以用作类似集合的数据结构(请注意,布尔类型的零值为false)。此示例遍历Nodes
的链接列表并打印其值。它使用Node
指针的映射来检测列表中的循环。
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
,否则如果n
不存在,则为 false
。无需使用二值形式来测试地图中n
的存在;零值默认值对我们有用。
有用的零值的另一个实例是切片的map。追加到nil切片只会分配一个新切片,因此将值附加到切片map是一种单一的方法;无需检查键是否存在。在以下示例中,切片people填充了Person
值。每个People
都有一个Name
和一个切片的Likes。该示例创建了一个map,将每个喜欢与一个喜欢它的人相关联。
Another instance of helpful zero values is a map of slices. Appending to a nil slice just allocates a new slice, so it's a one-liner to append a value to a map of slices; there's no need to check if the key exists. In the following example, the slice people is populated with Person
values. Each Person
has a Name
and a slice of Likes. The example creates a map to associate each like with a slice of people that like it.
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切片视为零长度切片,因此即使没有人喜欢奶酪或培根(尽管不太可能),后两个示例也可以使用。
键类型
如前所述, map 的键可以是任何可比较的类型。 语言规范 对此进行了精确定义, 但简而言之, 可比较的类型是布尔, 数字, 字符串, 指针, 通道和接口类型, 以及可用于仅包含那些类型. 切片, 集合和函数显然不在此列表中;这些类型不能使用 ==
进行比较, 并且不能用作集合键.
显然, 字符串, 整数和其他基本类型都可以用作映射键, 但是结构键可能出乎意料. 结构可以用于多维维度的关键数据. 例如, 此集合可以用于按国家/地区统计网页点击量:
hits := make(map[string]map[string]int)
这是字符串到 (string
到 int
的集合) 的集合. 外部集合的每个键都是具有内部集合的网页路径. 每个内部集合键是一个两个字母的国家/地区代码. 此表达式检索澳大利亚人加载文档页面的次数:
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")
另一方面, 使用带有结构键的单个集合的设计可以消除所有的复杂性:
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
.
This statement declares a counter
variable that is an anonymous struct containing a map and an embedded 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()
迭代顺序
当使用范围循环在地图上进行迭代时, 未指定迭代顺序, 并且不能保证每次迭代之间都相同. 如果需要稳定的迭代顺序, 则必须维护一个指定该顺序的单独的数据结构. 本示例使用单独的键排序片来按键顺序打印 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])
}
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。