(4-3)容器(Container):map映射

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

除了以上的基本操作外,还有一些其他的操作可以对映射进行操作,例如:

  1. 使用函数len()获取映射中“键-值”对的数量;
  2. 使用_, ok := m[key]判断映射中是否存在指定的键;
  3. 使用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中提供了如下常用的内置方法:

  1. Load(key interface{}) (value interface{}, ok bool):获取指定键对应的值,如果键不存在,则返回零值和 false。
  2. Store(key interface{}, value interface{}):设置指定键对应的值,如果键不存在,则会添加该键值对,如果键已经存在,则会更新该键的值。
  3. Delete(key interface{}):删除指定键的键值对,如果键不存在,则不会进行任何操作。
  4. 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.")
	}
}

对上述代码的具体说明如下:

  1. 创建了一个 sync.Map 对象 counter,用来存储计数器的值。然后,我们使用 100 个 goroutine 并发地向计数器中添加数值。具体的实现方式是,每个 goroutine 都从 counter 中获取键为 "counter" 的值,如果这个值已经存在,就将它加 1 后再存储回去;如果这个值不存在,就将它设为 1。
  2. 由于 sync.Map 是并发安全的,多个 goroutine 可以同时访问和修改它,而不会发生竞态条件。因此,在这个例子中,100 个 goroutine 可以同时向计数器中添加数值,而不会造成数据错误。
  3. 最后,我们从 counter 中获取键为 "counter" 的值,并输出它的结果。由于我们创建了 100 个 goroutine 来并发地添加计数器的值,因此最终的输出结果应该为 100。
  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值