4.3 map映射
在 Go 语言中,映射(Map)是一种无序的键值对的集合,也可以称为关联数组或字典。每个键必须是唯一的,而且键和值可以是任何类型,包括内置类型和自定义类型。map 是一种特殊的数据结构,是一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构。通过给定的key,可以迅速找到对应的value。
4.3.1 创建map
在 Go 语言中,可以使用内置函数make()来创建一个空的映射,也可以使用字面量的方式直接创建一个带有初始键值对的映射。下面分别介绍这两种方式的用法:
(1)使用函数make()创建空映射,具体语法格式如下:
m := make(map[keyType]valueType)
其中 keyType 表示键的类型,valueType 表示值的类型,例如:
m := make(map[string]int) // 创建一个字符串到整数的映射
(2)使用字面量创建带有初始键值对的映射
可以使用字面量的方式创建一个带有初始键值对的映射,具体语法格式如下:
m := map[keyType]valueType{
key1: value1,
key2: value2,
...
}
其中 key1、key2 等表示键,value1、value2 等表示值,例如:
m := map[string]int{
"apple": 1,
"banana": 2,
}
以上两种方式都可以创建一个映射,根据具体的需求来选择使用哪种方式。需要注意的是,映射是一种引用类型,如果将映射作为参数传递给函数或者赋值给另一个变量,那么实际上是传递了指向同一个底层数据结构的指针,所以对于同一个映射的操作,会影响到所有的引用它的变量。
注意:映射和集合是两个不同的概念
集合(Set)是一个不允许重复元素的无序集合,每个元素在集合中都是唯一的。集合通常支持基本的集合操作,例如添加元素、删除元素、判断元素是否存在等。而映射(Map)则是一种无序的键值对的集合,每个键必须是唯一的,而且键和值可以是任何类型。映射通常支持添加键值对、删除键值对、修改键值对等操作,还可以使用键来查询对应的值。
尽管映射和集合在实现细节上可能有相似之处,但是它们的目的和使用场景是不同的。集合通常用于处理元素的去重和无序存储,而映射则用于将一个键和一个值关联起来,以便于对键进行查询和值进行操作。
在 Go 语言中,集合和映射都可以使用内置的数据结构来实现,例如使用切片或 map 来实现集合,使用 map 来实现映射。需要根据实际的需求来选择合适的数据结构。
下面是一个简单的例子(源码路径:Go-codes\4\ying.go),展示了创建一个映射,并向映射中添加元素、修改元素、删除元素、以及遍历映射的过程。
// 创建一个映射
m := make(map[string]int)
// 添加元素
m["apple"] = 1
m["banana"] = 2
// 修改元素
m["apple"] = 3
// 删除元素
delete(m, "banana")
// 遍历映射
for key, value := range m {
fmt.Printf("%s -> %d\n", key, value)
}
在上面的代码中,函数make()用于创建一个空的映射,m["apple"] = 1语句用于向映射中添加键值对,m["apple"] = 3用于修改键值对中的值,delete(m, "banana")用于删除指定的键值对,最后使用for循环和range关键字遍历映射中的所有键值对。执行后会输出:
apple -> 3
除了以上的基本操作外,还有一些其他的操作可以对映射进行操作,例如:
- 使用函数len()获取映射中“键-值”对的数量;
- 使用_, ok := m[key]判断映射中是否存在指定的键;
- 使用map作为函数的参数和返回值,来实现更复杂的数据结构。
由于映射的容量是由 Go 运行时动态分配和管理的,因此不能像切片或数组那样直接获取其容量值。不过可以使用内置的 len 函数获取映射中键值对的数量,从而得知映射的实际容量。例如下面的例子(源码路径:Go-codes\4\liang.go)演示了这一用法。
m := make(map[string]int, 10) // 创建一个容量为 10 的映射
fmt.Println(len(m)) // 输出 0,表示映射中还没有键值对
m["apple"] = 1
m["banana"] = 2
fmt.Println(len(m)) // 输出 2,表示映射中已经有 2 个键值对
m["orange"] = 3
fmt.Println(len(m)) // 输出 3,表示映射中已经有 3 个键值对
在上述代码中,使用函数make()创建了一个容量为 10 的映射 m,然后向映射中添加了三个键值对。由于映射的容量是动态分配和管理的,因此我们不能直接获取映射的容量值,但是可以通过 len 函数获取映射中键值对的数量来推断其实际容量,因为映射的容量一般会根据实际情况进行动态扩容。执行后会输出:
0
2
3
注意:映射是一种引用类型,如果将映射作为参数传递给函数或者赋值给另一个变量,那么实际上是传递了指向同一个底层数据结构的指针,所以对于同一个映射的操作,会影响到所有的引用它的变量。
4.3.2 遍历map
在Go语言中,使用range关键字遍历map。例如下面的代码:
// 创建一个map
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 遍历map
for key, value := range m {
fmt.Println(key, value)
}
在上面的代码中,使用range关键字遍历了一个键为字符串类型,值为整数类型的map。在每次迭代中,key变量被赋值为当前键的值,value变量被赋值为当前键对应的值的值。输出结果:
a 1
b 2
c 3
4.3.3 map的删除和清空
1. map的删除
在Go语言中,可以使用 delete() 函数来删除map中的键值对。delete() 函数接受两个参数,第一个参数是要删除的map,第二个参数是要删除的键。下面是一个简单的例子,演示了使用函数delete()删除map中的键值对的方法。
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}
// 删除键为 "banana" 的键值对
delete(m, "banana")
// 输出删除后的map
fmt.Println(m)
}
在上述代码中,首先定义了一个包含三个键值对的map m,然后使用函数delete()删除了键为 "banana" 的键值对。最后,使用函数fmt.Println()打印输出删除后的map。输出结果为:
map[apple:1 orange:3]
可以看到键为 "banana" 的键值对已经被成功删除了。需要注意的是,如果删除的键不存在于map中,函数delete()将不会产生任何影响,也不会报错。
2. map的清空
清空一个 map 的方法很简单,可以使用 for range 循环遍历 map 中的所有元素,并逐个删除它们。也可以使用函数make()重新创建一个空的 map,以覆盖原来的 map。以下是两种清空 map 的方法:
(1)使用 for range 循环删除元素
例如在下面的示例中,使用 for range 循环遍历 map 中的所有键,并使用函数delete()逐个删除元素。
// 声明并初始化 map
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}
// 遍历 map 并逐个删除元素
for k := range m {
delete(m, k)
}
(2)使用函数make()重新创建一个空的 map
例如在下面的示例中,先声明并初始化一个 map,然后使用 make() 函数重新创建一个空的 map 覆盖原来的 map,从而实现了清空 map 的目的。
// 声明并初始化 map
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}
// 使用 make() 函数重新创建一个空的 map
m = make(map[string]int)
需要注意的是,当一个 map 中的元素被删除后,其长度变为 0,但是 map 本身并不会被销毁,仍然可以使用。
4.3.4 map的多键索引
在 Go 语言中,map 是一种键值对集合,其中每个键必须是唯一的。如果想要实现多键索引,可以使用嵌套 map 的方式来实现。例如,可以定义一个以多个键为索引的 map,其中每个键都是一个 map。
实例4-5:使用嵌套 map 实现多键索引(源码路径:Go-codes\4\duosuo.go)
实例文件duosuo.go的具体实现代码如下所示。
func main() {
// 定义一个以多个键为索引的 map
m := make(map[string]map[string]int)
// 添加元素
m["apple"] = make(map[string]int)
m["apple"]["red"] = 1
m["apple"]["green"] = 2
m["banana"] = make(map[string]int)
m["banana"]["yellow"] = 3
m["banana"]["green"] = 4
// 输出 map
fmt.Println(m)
// 访问元素
fmt.Println(m["apple"]["red"]) // 输出 1
fmt.Println(m["banana"]["green"]) // 输出 4
}
在上述代码中,我们定义了一个以多个键为索引的 map,其中每个键都是一个 map。然后我们向 map 中添加了两个键值对,其中键 apple 对应的值又是一个 map,其中包含两个键值对,键 red 对应的值为 1,键 green 对应的值为 2。键 banana 对应的值也是一个 map,其中包含两个键值对,键 yellow 对应的值为 3,键 green 对应的值为 4。执行后会输出:
map[apple:map[green:2 red:1] banana:map[green:4 yellow:3]]
1
4
注意:使用嵌套 map 实现多键索引的方式虽然可以满足一定的需求,但是由于其结构较为复杂,因此使用时需要谨慎考虑。同时,在使用 map 时也需要注意避免并发访问和写入冲突等问题。
4.3.5 sync.Map
sync.Map 是 Go 语言标准库 sync 包提供的一个并发安全的映射(Map)实现。sync.Map提供了一种高效、简单、安全的方式来在多个 Goroutine 中共享映射数据,可以用来替代 map 在并发场景下的使用。
注意:Goroutine是 Go 语言中的一种轻量级线程(lightweight thread),也可以称之为协程(coroutine)。与操作系统线程不同,Goroutine 是由 Go 运行时(Go runtime)管理的,而不是由操作系统内核管理的,因此 Goroutine 更轻量级、更高效。有关Goroutine的详细知识,将在本书后面的章节中进行讲解。
sync.Map 的特点是针对读写操作做了锁分离,即读操作不会阻塞其他读操作,写操作也不会阻塞读操作,从而实现了较高的并发度。同时,sync.Map 内部使用了哈希表来存储键值对,可以快速查找和修改映射中的数据,具有较高的效率。
在sync.Map中提供了如下常用的内置方法:
- Load(key interface{}) (value interface{}, ok bool):获取指定键对应的值,如果键不存在,则返回零值和 false。
- Store(key interface{}, value interface{}):设置指定键对应的值,如果键不存在,则会添加该键值对,如果键已经存在,则会更新该键的值。
- Delete(key interface{}):删除指定键的键值对,如果键不存在,则不会进行任何操作。
- Range(f func(key, value interface{}) bool):遍历映射中的所有键值对,并对每个键值对执行指定的函数,如果函数返回 false,则会停止遍历。
需要注意的是,sync.Map 中的键和值都是 interface{} 类型,因此可以存储任意类型的数据。同时,由于 sync.Map 是并发安全的,因此可以在多个 Goroutine 中同时对其进行读写操作,而不必担心数据竞争问题
实例4-6:并发安全计数器(源码路径:Go-codes\4\ji.go)
实例文件ji.go的具体实现代码如下所示。
package main
import (
"fmt"
"sync"
)
func main() {
var counter sync.Map
for i := 0; i < 100; i++ {
go func() {
key := "counter"
value, ok := counter.LoadOrStore(key, 0)
if ok {
counter.Store(key, value.(int)+1)
} else {
counter.Store(key, 1)
}
}()
}
// 等待所有 goroutine 执行完成
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
wg.Done()
}()
wg.Wait()
value, ok := counter.Load("counter")
if ok {
fmt.Println("Counter:", value)
} else {
fmt.Println("Counter not found.")
}
}
对上述代码的具体说明如下:
- 创建了一个 sync.Map 对象 counter,用来存储计数器的值。然后,我们使用 100 个 goroutine 并发地向计数器中添加数值。具体的实现方式是,每个 goroutine 都从 counter 中获取键为 "counter" 的值,如果这个值已经存在,就将它加 1 后再存储回去;如果这个值不存在,就将它设为 1。
- 由于 sync.Map 是并发安全的,多个 goroutine 可以同时访问和修改它,而不会发生竞态条件。因此,在这个例子中,100 个 goroutine 可以同时向计数器中添加数值,而不会造成数据错误。
- 最后,我们从 counter 中获取键为 "counter" 的值,并输出它的结果。由于我们创建了 100 个 goroutine 来并发地添加计数器的值,因此最终的输出结果应该为 100。