Go 语言字典 (Map)

发布于: 2020-11-19 17:07:53 作者: 犬小哈 最后修订时间: 2020-11-19 17:07:54

很多场景下都需要字典容器,它描述的是一种映射关系,如字典中的页码对应相关页内容一样。

Go 语言中提供的字典容器为 mapmap 使用散列表(hash)实现。

一、初始化字典 map

Go 语言中定义字典 map 格式如下:

map [keyType]valueType
  • keyType 表示键类型;
  • valueType 表示键对应的值类型;

注意:键和键对应的值总是以一对一的形式存在。

下面是一段 map 的示例代码:

package main

import "fmt"

func main()  {
	// 定义一个键类型为字符串,值类型为整型的 map
	m := make(map[string]int)

	// 向 map 中添加一个键为 “犬小哈教程”,值为 1 的映射关系
	key := "犬小哈教程"
	m[key] = 1

	// 输出 map 中键为 “犬小哈教程” 对应的值
	fmt.Println(m[key])

	// 尝试输出一个不能存在的键,会输出该值类型的默认值 
	n := m["犬小哈教程2"]

	fmt.Println(n)
}

代码输出:

1
0

图示:

go语言字典代码示例

上面代码最后,我们尝试从 map 中获取一个并不存在的键(key), 此时会输出值类型的默认值,整型的默认值为 0 。

当我们需要明确知道 map 中是否存在某个键(key)时,可以使用下面这种写法:

// 声明一个 ok 变量,用来接收对应键是否存在于 map 中
value, ok := m["犬小哈教程2"]

// 如果值不存在,则输出值
if !ok {
  fmt.Println(value)
}

1.1 字段 map 的另外一种初始化方式

字典 map 还存在另外一种初始化方式, 代码如下:

m := map[int](string){
		1: "www",
		2: "quanxiaoha",
		3: "com",
}

上面的这段代码并没有使用 make(), 而是通过大括号的方式来初始化字典 map, 有点像 JSON 格式一样,冒号左边的是键(key) , 右边的是值(value) ,键值对之间使用逗号分隔。

二、遍历字段 map

Go 语言中字典 map 的遍历需要使用 for range 循环,代码如下:

package main

import "fmt"

func main()  {
	m := map[int](string){
		1: "www",
		2: "quanxiaoha",
		3: "com",
	}

	// 通过 for range 遍历, 获取 key, value 值并打印
	for key, value := range m {
		fmt.Printf("key: %d, value: %s\n", key, value)
	}
}

代码输出如下:

key: 1, value: www
key: 2, value: quanxiaoha
key: 3, value: com

如果只需要遍历值,也可以通过 Go 语言变量 小节中说到的匿名变量来实现:

for _, value := range m {
		fmt.Printf("value: %s\n", value)
}

只遍历键时,通过下面这种方式:

for key := range m {
		fmt.Printf("key: %d\n", key)
}

注意: 字典 map 是一种无序的数据结构,不要期望输出时按照一定顺序输出。如果需要按顺序输出,请使用切片来完成。

三、删除字典 map 中键值对

通过内置函数 delete() 来删除键值对,格式如下:

delete(map, 键)
  • map 表示要删除的目标 map 对象;
  • 键表示要删除的 map 中 key 键。

示例代码如下:

package main

import "fmt"

func main()  {
	m := map[int](string){
		1: "www",
		2: "quanxiaoha",
		3: "com",
	}

	// 删除 map 中键为 1 的键值对
	delete(m, 1)

	// 通过 for range 遍历, 获取 key, value 值并打印
	for key, value := range m {
		fmt.Println(key, value)
	}
}

代码输出如下:

2 quanxiaoha
3 com

四、能够在并发环境下使用的字典 sync.Map

Go 语言中的 map 在并发环境下,只读是线程安全的,同时读写是线程不安全的。

下面这段代码演示了并发环境下读写 map 会出现的问题,代码如下:

package main

func main()  {
	// 初始化一个键为整型,值也为整型的 map
	m := make(map[int]int)

	// 开启一段并发代码
	go func() {
		// 无限循环往 map 里写值
		for {
			m[1] = 1
		}
	}()

	// 开启一段并发代码
	go func() {
		// 无限循环读取 map 数据
		for {
			_ = m[1]
		}
	}()

	// 死循环,让上面的并发代码在后台执行
	for {
	}
}

运行上面的代码,会报错如下:

fatal error: concurrent map read and map write

错误提示:因为并发的对 map 进行读写。两个并发函数不断的对 map 进行读写发生了竞态问题。map 内部会对这种并发操作进行检查并提前发现。

正常情况下,针对并发读写的场景,是需要加锁处理的。但是加锁就意味了性能不高。Go 语言在 1.9 版本中提供了一种高效率的并发安全的 sync.Map

sync.Map 有以下特性:

  • 无需初始化,直接声明即可;
  • sync.Map不能使用 map 的方式进行取值、设置等操作,而是使用 sync.Map提供的方法进行调用,如:Store 表示存储,Load 表示获取,Delete 表示删除。
  • 针对遍历操作,需要使用 Range 配合一个回调函数,回调函数会返回内部遍历出来的值。Range 参数中的回调函数返回值功能是: 需要继续遍历时,返回 true;终止遍历时,返回 false

示例代码如下:

package main

import (
	"fmt"
	"sync"
)

func main()  {
	var m sync.Map

	// 添加一些键值对到 map 中
	m.Store(1, "www")
	m.Store(2, "quanxiaoha")
	m.Store(3, "com")

	// 从 sync.Map 中获取键为 2 的值
	fmt.Println(m.Load(2))

	// 删除键值对
	m.Delete(1)

	// 遍历 sync.Map 中的键值对
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("key: %d, value: %s\n", key, value)
		return true
	})
}

代码输出如下:

quanxiaoha true
key: 2, value: quanxiaoha
key: 3, value: com

注意: sync.Map 没有提供获取 map 元素数量的方法,你需要自行遍历计数。