Go 语言中的映射操作
介绍
计算机科学中最有用的数据结构之一是哈希表。许多哈希表实现存在不同的属性,但通常它们提供了快速的查找、添加和删除。Go 提供了一个内置的映射类型,实现了哈希表。
声明和初始化
Go 的映射类型看起来像这样:
map[KeyType]ValueType
其中 KeyType 可能是任何可比较的类型(稍后会详细介绍),ValueType 可能是任何类型,包括另一个映射!这个变量 m 是一个字符串键到整数值的映射:
var m map[string]int
映射类型是引用类型,如指针或切片,因此上述 m 的值为 nil;它不指向已初始化的映射。读取时,nil 映射的行为就像空映射一样,但尝试写入 nil 映射将导致运行时panic;不要这样做。要初始化映射,请使用内置的 make 函数:
m = make(map[string]int)
make 函数分配并初始化哈希映射数据结构,并返回指向它的映射值。该数据结构的具体细节是运行时的实现细节,不由语言本身指定。在本文中,我们将专注于映射的使用,而不是它们的实现。
使用映射
Go 提供了熟悉的语法来操作映射。此语句将键 "route" 设置为值 66:
m["route"] = 66
此语句检索存储在键 "route" 下的值,并将其分配给新变量 i:
i := m["route"]
如果请求的键不存在,我们得到值类型的零值。在这种情况下,值类型是 int,所以零值是 0:
j := m["root"]
// j == 0
内置的 len 函数返回映射中的项目数量:
n := len(m)
内置的 delete 函数从映射中删除条目:
delete(m, "route")
delete 函数不返回任何内容,并且如果指定的键不存在,将不做任何事情。双值赋值测试键的存在:
i, ok := m["route"]
在此语句中,第一个值(i)被分配存储在键 "route" 下的值。如果该键不存在,i 是值类型的零值(0)。第二个值(ok)是一个布尔值,如果键存在于映射中,则为 true,否则为 false。要测试键而不检索值,请使用下划线代替第一个值:
_, ok := m["route"]
要遍历映射的内容,请使用 range 关键字:
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
要使用一些数据初始化映射,请使用映射文字:
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
相同的语法可用于初始化空映射,功能上与使用 make 函数相同:
m = map[string]int{}
利用零值
当键不存在时,映射检索产生零值可能很方便。例如,布尔值的映射可以用作集合样的数据结构(请记住,布尔类型的零值是 false)。此示例遍历节点的链表并打印其值。它使用节点指针的映射来检测列表中的循环。
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] 为 true 如果 n 已被访问,或 false 如果 n 不存在。我们无需使用双值形式测试映射中 n 的存在;零值默认为我们做了。
另一个有用的零值实例是切片的映射。附加到 nil 切片只会分配新切片,因此将值附加到切片的映射是一行代码;无需检查键是否存在。在以下示例中,切片 people 用 Person 值填充。每个 Person 都有一个 Name 和一个 Likes 切片。示例创建一个映射,将每个喜欢与一组喜欢它的人关联起来。
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 切片视为零长度切片,所以即使没有人喜欢奶酪或培根(不管可能性有多小),这两个示例也会起作用。
键类型
如前所述,映射键可以是任何可比较的类型。语言规范明确定义了这一点,但简而言之,可比较的类型是布尔、数字、字符串、指针、通道和接口类型,以及仅包含这些类型的结构或数组。值得注意的是,切片、映射和函数不在列表中;这些类型不能使用 == 进行比较,也不能用作映射键。显然,字符串、整数和其他基本类型应该可以用作映射键,但可能出乎意料的是结构键。结构可以用于按多个维度键入数据。例如,这个映射的映射可以用来统计网页点击量:
hits := make(map[string]map[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"}]
并发
映射不适合并发使用:当您同时读取和写入它们时,未定义会发生什么。如果您需要从并发执行的 goroutine 中读取和写入映射,则必须通过某种同步机制进行访问。保护映射的一种常见方法是使用 sync.RWMutex。此语句声明了一个计数器变量,该变量是一个包含映射和嵌入式 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[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 语言中映射的使用和操作,包括声明、初始化、工作方式、零值的利用、键类型、并发和迭代顺序等方面。希望这对您有所帮助!如果您有任何问题或需要进一步的解释,请随时提问。