上一篇我们学习了 Slice。Slice 非常适合保存一组动态数据,例如:
1users := []string{"Tom", "Jack", "Lucy"} 2
但是 Slice 主要通过数字下标访问元素:
1users[0] 2users[1] 3
实际开发中,我们经常希望通过用户名、商品编号、配置名称等直接查找数据。例如:
1"Tom" -> 90 2"Jack" -> 85 3"port" -> 8080 4"host" -> localhost 5
这种“一个 Key 对应一个 Value”的数据结构,就是 Go 中的 Map。Map 是 Go 开发中使用频率非常高的数据结构,配置管理、缓存、数据统计、JSON 处理、数据库结果整理等场景都会大量使用。
一、什么是 Map
Map 是一种 Key-Value 键值对数据结构。
基本形式:
1Key -> Value 2
例如:
1Tom -> 90 2Jack -> 85 3Lucy -> 96 4
如果使用 Slice 保存成绩,可能需要:
1names := []string{"Tom", "Jack", "Lucy"} 2scores := []int{90, 85, 96} 3
而 Map 可以直接表示:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4 "Lucy": 96, 5} 6
查询 Tom 的成绩:
1fmt.Println(scores["Tom"]) 2
输出:
190 2
相比数字下标,通过具有业务含义的 Key 查找数据通常更加直观。
二、Map 的基本语法
Map 类型写法:
1map[Key类型]Value类型 2
例如:
1map[string]int 2
表示:
1Key = string 2Value = int 3
也可以:
1map[int]string 2
表示整数作为 Key,字符串作为 Value。
例如:
1users := map[int]string{ 2 1: "Tom", 3 2: "Jack", 4 3: "Lucy", 5} 6
访问:
1fmt.Println(users[2]) 2
输出:
1Jack 2
三、定义 Map
可以先声明:
1var scores map[string]int 2
但此时 scores 是 nil map:
1fmt.Println(scores == nil) 2
输出:
1true 2
nil map 可以读取,但是不能直接写入:
1scores["Tom"] = 90 2
这样运行时会发生错误。因此需要先初始化 Map。
四、使用 make 创建 Map
最常见的方式是:
1scores := make(map[string]int) 2
然后添加数据:
1scores["Tom"] = 90 2scores["Jack"] = 85 3scores["Lucy"] = 96 4
完整示例:
1package main 2import "fmt" 3func main() { 4 scores := make(map[string]int) 5 scores["Tom"] = 90 6 scores["Jack"] = 85 7 scores["Lucy"] = 96 8 fmt.Println(scores) 9} 10
还可以给 make 提供一个初始容量提示:
1scores := make(map[string]int, 100) 2
如果预计需要保存较多数据,这种方式可以减少运行过程中重新分配内部存储的开销。不过这个数字不是固定长度,Map 仍然可以继续增加元素。
五、创建 Map 时直接初始化
如果数据已经确定,可以直接:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4 "Lucy": 96, 5} 6
字符串 Map:
1config := map[string]string{ 2 "host": "localhost", 3 "port": "8080", 4 "mode": "debug", 5} 6
访问:
1fmt.Println(config["host"]) 2
输出:
1localhost 2
六、添加和修改数据
Map 添加数据非常简单:
1scores := make(map[string]int) 2scores["Tom"] = 90 3
如果 Key 不存在,就是新增。
继续:
1scores["Jack"] = 85 2
如果 Key 已经存在:
1scores["Tom"] = 100 2
就是修改。
因此 Map 添加和修改使用相同语法:
1m[key] = value 2
Map 中的 Key 不能重复,同一个 Key 只能对应一个当前 Value。
七、读取 Map 数据
读取数据:
1scores := map[string]int{ 2 "Tom": 90, 3} 4fmt.Println(scores["Tom"]) 5
输出:
190 2
但有一个非常重要的问题。如果访问不存在的 Key:
1fmt.Println(scores["Jack"]) 2
不会直接报错,而是返回 Value 类型的零值。
由于 Value 是 int,因此得到:
10 2
如果 Value 是字符串:
1config := map[string]string{} 2fmt.Println(config["host"]) 3
得到空字符串。
因此仅根据返回值,有时候无法判断 Key 到底存在还是不存在。
八、判断 Key 是否存在
Go 提供了非常经典的 Map 查询写法:
1value, ok := scores["Tom"] 2
其中:
1value = 对应的数据 2ok = Key 是否存在 3
例如:
1score, ok := scores["Tom"] 2if ok { 3 fmt.Println("成绩:", score) 4} else { 5 fmt.Println("用户不存在") 6} 7
也可以直接:
1if score, ok := scores["Tom"]; ok { 2 fmt.Println(score) 3} 4
这是 Go 项目中非常常见的写法。
例如:
1scores := map[string]int{ 2 "Tom": 0, 3} 4
如果只执行:
1score := scores["Tom"] 2
得到 0,但无法判断是 Tom 的成绩真的为 0,还是 Tom 不存在。
使用:
1score, ok := scores["Tom"] 2
就可以准确区分。
九、删除 Map 数据
Go 提供内置函数:
1delete() 2
例如:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4} 5delete(scores, "Tom") 6fmt.Println(scores) 7
Tom 对应的数据就被删除了。
基本语法:
1delete(map变量, key) 2
如果删除一个不存在的 Key:
1delete(scores, "Lucy") 2
也不会报错。
十、获取 Map 元素数量
可以使用:
1len() 2
例如:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4 "Lucy": 96, 5} 6fmt.Println(len(scores)) 7
输出:
13 2
添加:
1scores["Bob"] = 88 2
此时:
1len(scores) 2
就是 4。
删除:
1delete(scores, "Tom") 2
长度又会减少。
十一、遍历 Map
Map 通常使用 range 遍历:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4 "Lucy": 96, 5} 6for key, value := range scores { 7 fmt.Println(key, value) 8} 9
如果只需要 Key:
1for key := range scores { 2 fmt.Println(key) 3} 4
如果只需要 Value:
1for _, value := range scores { 2 fmt.Println(value) 3} 4
需要特别注意:不要依赖 Map 的遍历顺序。
不能认为:
1for key, value := range scores { 2 // 每次都会按照插入顺序执行 3} 4
Go 不保证 Map 的遍历顺序。如果业务要求固定顺序,通常需要单独保存 Key,然后排序。
十二、按照 Key 排序输出
例如:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4 "Lucy": 96, 5} 6
先提取所有 Key:
1keys := make([]string, 0, len(scores)) 2for key := range scores { 3 keys = append(keys, key) 4} 5
然后排序:
1sort.Strings(keys) 2
最后按照排序后的 Key 访问:
1for _, key := range keys { 2 fmt.Println(key, scores[key]) 3} 4
完整代码:
1package main 2import ( 3 "fmt" 4 "sort" 5) 6func main() { 7 scores := map[string]int{ 8 "Tom": 90, 9 "Jack": 85, 10 "Lucy": 96, 11 } 12 keys := make([]string, 0, len(scores)) 13 for key := range scores { 14 keys = append(keys, key) 15 } 16 sort.Strings(keys) 17 for _, key := range keys { 18 fmt.Println(key, scores[key]) 19 } 20} 21
这也是 Slice 和 Map 配合使用的典型场景。
十三、Map 的 Key 有什么要求
并不是所有类型都能作为 Map 的 Key。
Map 的 Key 必须是可以使用 == 和 != 比较的类型。
常见可以作为 Key 的类型包括:
1string 2int 3bool 4数组 5指针 6部分 struct 7
例如:
1map[string]int 2map[int]string 3
都是非常常见的。
Slice 不能直接作为 Map Key:
1map[[]int]string 2
这是错误的,因为 Slice 不能直接使用 == 比较两个切片的内容。
Map 本身和函数类型也不能作为 Map Key。
十四、Map 的 Value 可以很复杂
Map 的 Value 不仅可以是基本类型,还可以是 Slice、Map、Struct 等。
例如 Value 是 Slice:
1users := map[string][]string{ 2 "admin": {"Tom", "Jack"}, 3 "user": {"Lucy", "Bob"}, 4} 5
访问:
1fmt.Println(users["admin"]) 2
输出:
1[Tom Jack] 2
也可以追加:
1users["admin"] = append(users["admin"], "Mike") 2
这种 map[string][]string 在实际开发中非常常见。
十五、嵌套 Map
Map 的 Value 还可以继续是 Map:
1users := map[string]map[string]string{ 2 "1001": { 3 "name": "Tom", 4 "age": "20", 5 }, 6 "1002": { 7 "name": "Jack", 8 "age": "25", 9 }, 10} 11
访问:
1fmt.Println(users["1001"]["name"]) 2
输出:
1Tom 2
不过当数据结构越来越复杂时,通常更推荐使用 Struct,而不是无限嵌套 Map,因为 Struct 类型更加明确,也更容易维护。
十六、Map 作为函数参数
Map 可以直接作为函数参数:
1func change(scores map[string]int) { 2 scores["Tom"] = 100 3} 4
调用:
1scores := map[string]int{ 2 "Tom": 90, 3} 4change(scores) 5fmt.Println(scores["Tom"]) 6
输出:
1100 2
这说明函数中修改 Map 内容可以影响调用方看到的数据。
因此通常没有必要写:
1func change(scores *map[string]int) 2
直接传:
1func change(scores map[string]int) 2
一般就可以完成对 Map 内容的修改。
十七、Map 不能直接比较
两个 Map 不能直接:
1a == b 2
例如:
1a := map[string]int{"Tom": 90} 2b := map[string]int{"Tom": 90} 3
下面这样是不允许的:
1fmt.Println(a == b) 2
Map 只能和 nil 比较:
1if a == nil { 2 fmt.Println("nil map") 3} 4
如果需要比较两个 Map 的内容,可以自己遍历比较,或者在合适场景下使用标准库提供的相关工具。
十八、nil Map 和空 Map
下面是 nil Map:
1var a map[string]int 2
此时:
1a == nil 2
为 true。
下面是已经初始化但没有数据的 Map:
1b := make(map[string]int) 2
此时:
1b == nil 2
为 false。
两者:
1len(a) 2len(b) 3
都是 0。
最大的区别之一是 nil Map 不能写入:
1a["Tom"] = 90 2
会发生运行时错误。
而:
1b["Tom"] = 90 2
可以正常执行。
因此如果准备向 Map 写入数据,应先使用 make() 或字面量初始化。
十九、Map 实战:统计单词出现次数
Map 非常适合进行数据统计。
例如:
1words := []string{ 2 "go", "java", "go", "rust", 3 "go", "java", 4} 5
统计每个单词出现次数:
1package main 2import "fmt" 3func main() { 4 words := []string{ 5 "go", "java", "go", 6 "rust", "go", "java", 7 } 8 counts := make(map[string]int) 9 for _, word := range words { 10 counts[word]++ 11 } 12 for word, count := range counts { 13 fmt.Println(word, count) 14 } 15} 16
核心代码只有:
1counts[word]++ 2
如果 Key 不存在:
1counts[word] 2
默认得到 0,然后执行 ++,第一次就变成 1。
最终数据类似:
1go -> 3 2java -> 2 3rust -> 1 4
这就是 Map 非常典型的使用方式。
二十、Map 实战:用户信息查询
例如保存用户 ID 和用户名:
1users := map[int]string{ 2 1001: "Tom", 3 1002: "Jack", 4 1003: "Lucy", 5} 6
查询:
1id := 1002 2if name, ok := users[id]; ok { 3 fmt.Println("用户:", name) 4} else { 5 fmt.Println("用户不存在") 6} 7
这种结构可以用于缓存、配置映射、状态映射等场景。
二十一、并发使用 Map 要注意
普通 Map 不适合在没有同步保护的情况下进行并发读写。
例如多个 goroutine 同时修改:
1m["count"]++ 2
可能产生并发安全问题。
后面学习并发编程时,可以使用:
1sync.Mutex 2
对共享 Map 进行保护,或者根据具体场景使用:
1sync.Map 2
因此现阶段先记住:
1普通 Map 不要随意进行无同步的并发读写。 2
二十二、Slice 和 Map 怎么选择
如果数据主要按照位置保存:
1第0个 2第1个 3第2个 4
通常使用 Slice:
1[]string 2
如果需要根据 Key 查找:
1userID -> user 2name -> score 3config -> value 4
通常使用 Map:
1map[string]int 2
例如用户列表:
1users := []string{"Tom", "Jack", "Lucy"} 2
适合 Slice。
用户 ID 对应用户名:
1users := map[int]string{ 2 1001: "Tom", 3 1002: "Jack", 4} 5
则更加适合 Map。
二十三、Map 常见错误
第一个错误是没有初始化就写入:
1var m map[string]int 2m["Tom"] = 90 3
应该:
1m := make(map[string]int) 2
第二个错误是无法区分零值和 Key 不存在:
1value := m["Tom"] 2
更可靠的方式:
1value, ok := m["Tom"] 2
第三个错误是依赖 Map 遍历顺序:
1for key := range m { 2} 3
Map 不保证遍历顺序。
第四个错误是使用不支持比较的类型作为 Key:
1map[[]int]string 2
Slice 不能作为 Map Key。
第五个错误是多个 goroutine 无保护地同时读写普通 Map,在并发程序中必须特别注意。
二十四、总结
Map 是 Go 中非常重要的键值数据结构。
定义:
1var scores map[string]int 2
创建:
1scores := make(map[string]int) 2
初始化:
1scores := map[string]int{ 2 "Tom": 90, 3 "Jack": 85, 4} 5
添加:
1scores["Lucy"] = 96 2
修改:
1scores["Tom"] = 100 2
读取:
1score := scores["Tom"] 2
判断 Key:
1score, ok := scores["Tom"] 2
删除:
1delete(scores, "Tom") 2
长度:
1len(scores) 2
遍历:
1for key, value := range scores { 2 fmt.Println(key, value) 3} 4
学习 Map 重点掌握:
11.Map 使用 Key-Value 保存数据 22.Key 必须是可比较类型 33.读取不存在的 Key 会返回 Value 的零值 44.使用 value, ok 判断 Key 是否存在 55.delete 可以删除指定 Key 66.Map 遍历顺序不固定 77.nil Map 可以读取但不能写入 88.普通 Map 并发读写需要同步保护 9
到这里,我们已经学习了数组 Array、切片 Slice 和 Map,这三种数据结构能够解决大量集合数据存储问题。但前面的内容中还有一个非常重要的问题:当变量传递给函数以后,到底是在操作原来的数据,还是操作一份副本?为什么有时候修改函数参数不会影响外部变量,而 Map、Slice 又表现得有所不同?要真正理解这些问题,就需要掌握 Go 中非常重要的基础概念——指针 Pointer。
下一篇:指针 Pointer——理解地址、取址与解引用
《Go 编程实战:Map——使用 Key-Value 管理键值数据》 是转载文章,点击查看原文。
