Go 编程实战:Map——使用 Key-Value 管理键值数据

0 阅读10分钟

上一篇我们学习了 Slice。Slice 非常适合保存一组动态数据,例如:

users := []string{"Tom", "Jack", "Lucy"}

但是 Slice 主要通过数字下标访问元素:

users[0]
users[1]

实际开发中,我们经常希望通过用户名、商品编号、配置名称等直接查找数据。例如:

"Tom" -> 90
"Jack" -> 85
"port" -> 8080
"host" -> localhost

这种“一个 Key 对应一个 Value”的数据结构,就是 Go 中的 Map。Map 是 Go 开发中使用频率非常高的数据结构,配置管理、缓存、数据统计、JSON 处理、数据库结果整理等场景都会大量使用。

一、什么是 Map

Map 是一种 Key-Value 键值对数据结构。
基本形式:

Key -> Value

例如:

Tom  -> 90
Jack -> 85
Lucy -> 96

如果使用 Slice 保存成绩,可能需要:

names := []string{"Tom", "Jack", "Lucy"}
scores := []int{90, 85, 96}

而 Map 可以直接表示:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
	"Lucy": 96,
}

查询 Tom 的成绩:

fmt.Println(scores["Tom"])

输出:

90

相比数字下标,通过具有业务含义的 Key 查找数据通常更加直观。

二、Map 的基本语法

Map 类型写法:

map[Key类型]Value类型

例如:

map[string]int

表示:

Key   = string
Value = int

也可以:

map[int]string

表示整数作为 Key,字符串作为 Value。
例如:

users := map[int]string{
	1: "Tom",
	2: "Jack",
	3: "Lucy",
}

访问:

fmt.Println(users[2])

输出:

Jack

三、定义 Map

可以先声明:

var scores map[string]int

但此时 scoresnil map

fmt.Println(scores == nil)

输出:

true

nil map 可以读取,但是不能直接写入:

scores["Tom"] = 90

这样运行时会发生错误。因此需要先初始化 Map。

四、使用 make 创建 Map

最常见的方式是:

scores := make(map[string]int)

然后添加数据:

scores["Tom"] = 90
scores["Jack"] = 85
scores["Lucy"] = 96

完整示例:

package main
import "fmt"
func main() {
	scores := make(map[string]int)
	scores["Tom"] = 90
	scores["Jack"] = 85
	scores["Lucy"] = 96
	fmt.Println(scores)
}

还可以给 make 提供一个初始容量提示:

scores := make(map[string]int, 100)

如果预计需要保存较多数据,这种方式可以减少运行过程中重新分配内部存储的开销。不过这个数字不是固定长度,Map 仍然可以继续增加元素。

五、创建 Map 时直接初始化

如果数据已经确定,可以直接:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
	"Lucy": 96,
}

字符串 Map:

config := map[string]string{
	"host": "localhost",
	"port": "8080",
	"mode": "debug",
}

访问:

fmt.Println(config["host"])

输出:

localhost

六、添加和修改数据

Map 添加数据非常简单:

scores := make(map[string]int)
scores["Tom"] = 90

如果 Key 不存在,就是新增。
继续:

scores["Jack"] = 85

如果 Key 已经存在:

scores["Tom"] = 100

就是修改。
因此 Map 添加和修改使用相同语法:

m[key] = value

Map 中的 Key 不能重复,同一个 Key 只能对应一个当前 Value。

七、读取 Map 数据

读取数据:

scores := map[string]int{
	"Tom": 90,
}
fmt.Println(scores["Tom"])

输出:

90

但有一个非常重要的问题。如果访问不存在的 Key:

fmt.Println(scores["Jack"])

不会直接报错,而是返回 Value 类型的零值。
由于 Value 是 int,因此得到:

0

如果 Value 是字符串:

config := map[string]string{}
fmt.Println(config["host"])

得到空字符串。
因此仅根据返回值,有时候无法判断 Key 到底存在还是不存在。

八、判断 Key 是否存在

Go 提供了非常经典的 Map 查询写法:

value, ok := scores["Tom"]

其中:

value = 对应的数据
ok    = Key 是否存在

例如:

score, ok := scores["Tom"]
if ok {
	fmt.Println("成绩:", score)
} else {
	fmt.Println("用户不存在")
}

也可以直接:

if score, ok := scores["Tom"]; ok {
	fmt.Println(score)
}

这是 Go 项目中非常常见的写法。
例如:

scores := map[string]int{
	"Tom": 0,
}

如果只执行:

score := scores["Tom"]

得到 0,但无法判断是 Tom 的成绩真的为 0,还是 Tom 不存在。
使用:

score, ok := scores["Tom"]

就可以准确区分。

九、删除 Map 数据

Go 提供内置函数:

delete()

例如:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
}
delete(scores, "Tom")
fmt.Println(scores)

Tom 对应的数据就被删除了。
基本语法:

delete(map变量, key)

如果删除一个不存在的 Key:

delete(scores, "Lucy")

也不会报错。

十、获取 Map 元素数量

可以使用:

len()

例如:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
	"Lucy": 96,
}
fmt.Println(len(scores))

输出:

3

添加:

scores["Bob"] = 88

此时:

len(scores)

就是 4。
删除:

delete(scores, "Tom")

长度又会减少。

十一、遍历 Map

Map 通常使用 range 遍历:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
	"Lucy": 96,
}
for key, value := range scores {
	fmt.Println(key, value)
}

如果只需要 Key:

for key := range scores {
	fmt.Println(key)
}

如果只需要 Value:

for _, value := range scores {
	fmt.Println(value)
}

需要特别注意:不要依赖 Map 的遍历顺序。
不能认为:

for key, value := range scores {
	// 每次都会按照插入顺序执行
}

Go 不保证 Map 的遍历顺序。如果业务要求固定顺序,通常需要单独保存 Key,然后排序。

十二、按照 Key 排序输出

例如:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
	"Lucy": 96,
}

先提取所有 Key:

keys := make([]string, 0, len(scores))
for key := range scores {
	keys = append(keys, key)
}

然后排序:

sort.Strings(keys)

最后按照排序后的 Key 访问:

for _, key := range keys {
	fmt.Println(key, scores[key])
}

完整代码:

package main
import (
	"fmt"
	"sort"
)
func main() {
	scores := map[string]int{
		"Tom":  90,
		"Jack": 85,
		"Lucy": 96,
	}
	keys := make([]string, 0, len(scores))
	for key := range scores {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		fmt.Println(key, scores[key])
	}
}

这也是 Slice 和 Map 配合使用的典型场景。

十三、Map 的 Key 有什么要求

并不是所有类型都能作为 Map 的 Key。
Map 的 Key 必须是可以使用 ==!= 比较的类型。
常见可以作为 Key 的类型包括:

string
int
bool
数组
指针
部分 struct

例如:

map[string]int
map[int]string

都是非常常见的。
Slice 不能直接作为 Map Key:

map[[]int]string

这是错误的,因为 Slice 不能直接使用 == 比较两个切片的内容。
Map 本身和函数类型也不能作为 Map Key。

十四、Map 的 Value 可以很复杂

Map 的 Value 不仅可以是基本类型,还可以是 Slice、Map、Struct 等。
例如 Value 是 Slice:

users := map[string][]string{
	"admin": {"Tom", "Jack"},
	"user":  {"Lucy", "Bob"},
}

访问:

fmt.Println(users["admin"])

输出:

[Tom Jack]

也可以追加:

users["admin"] = append(users["admin"], "Mike")

这种 map[string][]string 在实际开发中非常常见。

十五、嵌套 Map

Map 的 Value 还可以继续是 Map:

users := map[string]map[string]string{
	"1001": {
		"name": "Tom",
		"age":  "20",
	},
	"1002": {
		"name": "Jack",
		"age":  "25",
	},
}

访问:

fmt.Println(users["1001"]["name"])

输出:

Tom

不过当数据结构越来越复杂时,通常更推荐使用 Struct,而不是无限嵌套 Map,因为 Struct 类型更加明确,也更容易维护。

十六、Map 作为函数参数

Map 可以直接作为函数参数:

func change(scores map[string]int) {
	scores["Tom"] = 100
}

调用:

scores := map[string]int{
	"Tom": 90,
}
change(scores)
fmt.Println(scores["Tom"])

输出:

100

这说明函数中修改 Map 内容可以影响调用方看到的数据。
因此通常没有必要写:

func change(scores *map[string]int)

直接传:

func change(scores map[string]int)

一般就可以完成对 Map 内容的修改。

十七、Map 不能直接比较

两个 Map 不能直接:

a == b

例如:

a := map[string]int{"Tom": 90}
b := map[string]int{"Tom": 90}

下面这样是不允许的:

fmt.Println(a == b)

Map 只能和 nil 比较:

if a == nil {
	fmt.Println("nil map")
}

如果需要比较两个 Map 的内容,可以自己遍历比较,或者在合适场景下使用标准库提供的相关工具。

十八、nil Map 和空 Map

下面是 nil Map:

var a map[string]int

此时:

a == nil

true
下面是已经初始化但没有数据的 Map:

b := make(map[string]int)

此时:

b == nil

false
两者:

len(a)
len(b)

都是 0。
最大的区别之一是 nil Map 不能写入:

a["Tom"] = 90

会发生运行时错误。
而:

b["Tom"] = 90

可以正常执行。
因此如果准备向 Map 写入数据,应先使用 make() 或字面量初始化。

十九、Map 实战:统计单词出现次数

Map 非常适合进行数据统计。
例如:

words := []string{
	"go", "java", "go", "rust",
	"go", "java",
}

统计每个单词出现次数:

package main
import "fmt"
func main() {
	words := []string{
		"go", "java", "go",
		"rust", "go", "java",
	}
	counts := make(map[string]int)
	for _, word := range words {
		counts[word]++
	}
	for word, count := range counts {
		fmt.Println(word, count)
	}
}

核心代码只有:

counts[word]++

如果 Key 不存在:

counts[word]

默认得到 0,然后执行 ++,第一次就变成 1。
最终数据类似:

go   -> 3
java -> 2
rust -> 1

这就是 Map 非常典型的使用方式。

二十、Map 实战:用户信息查询

例如保存用户 ID 和用户名:

users := map[int]string{
	1001: "Tom",
	1002: "Jack",
	1003: "Lucy",
}

查询:

id := 1002
if name, ok := users[id]; ok {
	fmt.Println("用户:", name)
} else {
	fmt.Println("用户不存在")
}

这种结构可以用于缓存、配置映射、状态映射等场景。

二十一、并发使用 Map 要注意

普通 Map 不适合在没有同步保护的情况下进行并发读写。
例如多个 goroutine 同时修改:

m["count"]++

可能产生并发安全问题。
后面学习并发编程时,可以使用:

sync.Mutex

对共享 Map 进行保护,或者根据具体场景使用:

sync.Map

因此现阶段先记住:

普通 Map 不要随意进行无同步的并发读写。

二十二、Slice 和 Map 怎么选择

如果数据主要按照位置保存:

第0个
第1个
第2个

通常使用 Slice:

[]string

如果需要根据 Key 查找:

userID -> user
name   -> score
config -> value

通常使用 Map:

map[string]int

例如用户列表:

users := []string{"Tom", "Jack", "Lucy"}

适合 Slice。
用户 ID 对应用户名:

users := map[int]string{
	1001: "Tom",
	1002: "Jack",
}

则更加适合 Map。

二十三、Map 常见错误

第一个错误是没有初始化就写入:

var m map[string]int
m["Tom"] = 90

应该:

m := make(map[string]int)

第二个错误是无法区分零值和 Key 不存在:

value := m["Tom"]

更可靠的方式:

value, ok := m["Tom"]

第三个错误是依赖 Map 遍历顺序:

for key := range m {
}

Map 不保证遍历顺序。
第四个错误是使用不支持比较的类型作为 Key:

map[[]int]string

Slice 不能作为 Map Key。
第五个错误是多个 goroutine 无保护地同时读写普通 Map,在并发程序中必须特别注意。

二十四、总结

Map 是 Go 中非常重要的键值数据结构。
定义:

var scores map[string]int

创建:

scores := make(map[string]int)

初始化:

scores := map[string]int{
	"Tom":  90,
	"Jack": 85,
}

添加:

scores["Lucy"] = 96

修改:

scores["Tom"] = 100

读取:

score := scores["Tom"]

判断 Key:

score, ok := scores["Tom"]

删除:

delete(scores, "Tom")

长度:

len(scores)

遍历:

for key, value := range scores {
	fmt.Println(key, value)
}

学习 Map 重点掌握:

1.Map 使用 Key-Value 保存数据
2.Key 必须是可比较类型
3.读取不存在的 Key 会返回 Value 的零值
4.使用 value, ok 判断 Key 是否存在
5.delete 可以删除指定 Key
6.Map 遍历顺序不固定
7.nil Map 可以读取但不能写入
8.普通 Map 并发读写需要同步保护

到这里,我们已经学习了数组 Array、切片 Slice 和 Map,这三种数据结构能够解决大量集合数据存储问题。但前面的内容中还有一个非常重要的问题:当变量传递给函数以后,到底是在操作原来的数据,还是操作一份副本?为什么有时候修改函数参数不会影响外部变量,而 Map、Slice 又表现得有所不同?要真正理解这些问题,就需要掌握 Go 中非常重要的基础概念——指针 Pointer
下一篇:指针 Pointer——理解地址、取址与解引用