Go 从 0 到精通 · 第 10 课:映射 map
Go 从 0 到精通 · 第 10 课:映射 map
学习定位:这是整套 Go 教程的第 10 课。
前置要求:已经完成第 9 课,掌握了切片的创建、append、长度与容量、截取与共享。
本课目标:掌握 map 的创建、增删改查、遍历,理解判断键是否存在的语法,知道 map 是引用类型的特点。
1. 本课你要解决的核心问题
切片用下标(0, 1, 2, …)来定位元素。但现实中很多数据不适合用数字下标来索引:
- 用学号查学生姓名
- 用单词查释义
- 用 URL 统计访问次数
这些场景需要的是"通过一个键(key)找到对应的值(value)"——这就是 map 的用武之地。
你需要搞明白以下问题:
- map 怎么创建、怎么存取数据
- 怎么判断一个键是否存在
- 怎么删除键值对
- 怎么遍历 map
- map 和切片有什么区别
- map 有哪些常见坑
学完这一课,你就能用键值对结构来建模查找型的数据关系了。
2. map 的基本概念
2.1 什么是 map
map 是一种键值对(key-value)集合:
- 每个键(key)在 map 中唯一
- 每个键对应一个值(value)
- 通过键可以快速找到对应的值
打个比方:map 就像一本字典——你查"apple"(键),能找到"苹果"(值)。
2.2 类型声明
1 | map[键类型]值类型 |
例如:
map[string]int:键是字符串,值是整数map[int]string:键是整数,值是字符串map[string][]string:键是字符串,值是字符串切片
3. map 的创建
3.1 用字面量创建
1 | package main |
输出:
1 | map[小刚:92 小明:90 小红:85] |
注意:map 的打印顺序不固定。map 不保证遍历顺序。
3.2 用 make 创建
1 | package main |
make(map[string]int) 创建一个空的 map,可以直接使用。
3.3 nil map
1 | var m map[string]int // m 是 nil |
nil map 可以读取(返回零值),但不能写入:
1 | var m map[string]int |
使用 map 前一定要初始化(用 make 或字面量)。
4. 增删改查
4.1 添加和修改
1 | m := map[string]int{} |
添加和修改用的是同一个语法:m[key] = value。键不存在就添加,存在就更新。
4.2 查找
1 | m := map[string]int{"小明": 90, "小红": 85} |
直接用 m[key] 查找。如果键不存在,返回值类型的零值(int 是 0,string 是 "")。
但这有一个问题:m["小王"] 返回 0,你无法区分"小王的分数是 0"和"小王不存在"。
4.3 判断键是否存在(重要)
Go 提供了"逗号 ok"模式来判断键是否存在:
1 | m := map[string]int{"小明": 90, "小红": 85} |
- 第一个返回值是值
- 第二个返回值
ok是bool:键存在为true,不存在为false
这是 Go 中非常常见的模式,一定要掌握。
4.4 删除
1 | m := map[string]int{"小明": 90, "小红": 85, "小刚": 92} |
delete(map, key) 删除指定键的键值对。如果键不存在,什么都不做(不报错)。
5. map 的遍历
5.1 用 for range 遍历
1 | package main |
for range 遍历 map 返回两个值:键和值。
5.2 遍历顺序不固定
1 | for name, score := range scores { |
Go 从 1.12 开始,map 的遍历顺序是随机的。不要依赖 map 的遍历顺序。
如果需要有序遍历,先提取键并排序,再按顺序查 map。
5.3 只遍历键或只遍历值
1 | // 只遍历键 |
6. map 的零值与初始化
6.1 三种状态对比
1 | var a map[string]int // nil map |
| 状态 | == nil |
能读 | 能写 | 能 len |
|---|---|---|---|---|
nil map |
true |
能(返回零值) | panic | 能(返回 0) |
| 空 map | false |
能 | 能 | 能 |
关键:nil map 只能读不能写。使用 map 前务必初始化。
6.2 make 的第二个参数
1 | m := make(map[string]int, 100) |
第二个参数 100 是预估的元素数量提示。它不影响 map 的行为(不像切片的 cap),只是给运行时一个提示,让它分配更合适的初始内存,减少后续扩容次数。
7. map 作为函数参数
7.1 map 是引用类型
1 | package main |
map 传给函数后,函数内外共享同一个 map。在函数里增删改查都会影响外部。
这和切片不同(切片的 append 在函数内不影响外部),也和数组不同(数组传参完全复制)。
7.2 但 map 本身仍然是值传递
严格来说,Go 还是值传递——传给函数的是 map header 的副本。但 map header 内部包含指向底层哈希表的指针,所以函数内外共享同一个底层数据。
你不需要太纠结这个细节,只要记住:map 在函数内修改会影响外部。
8. map 的常见用法
8.1 统计词频
1 | package main |
输出:
1 | apple: 3 |
freq[w]++ 是一个很优雅的模式:如果 w 不存在,freq[w] 返回 0,++ 后变成 1。
8.2 用 map 模拟集合(Set)
Go 没有内置的 Set 类型,可以用 map[T]struct{} 实现:
1 | package main |
map[string]struct{} 的值类型是空结构体 struct{},它不占内存。这种模式只关心"键是否存在"。
8.3 map 嵌套切片
1 | students := map[string][]int{ |
9. 一段综合示例
1 | package main |
输出:
1 | 橙子库存:20 |
10. 常见坑总结
10.1 使用未初始化的 nil map
1 | var m map[string]int |
防范:用 make 或字面量初始化。
10.2 无法区分"键不存在"和"值为零值"
1 | m := map[string]int{"小明": 0} |
防范:用 val, ok := m[key] 判断。
10.3 以为遍历顺序固定
1 | m := map[int]string{1: "a", 2: "b", 3: "c"} |
防范:需要有序遍历时,先提取键并排序。
10.4 在并发中使用普通 map
普通的 map 不是并发安全的。多个 goroutine 同时读写一个 map 可能导致 panic。
防范:并发场景下使用 sync.Map 或加锁保护。
10.5 混淆切片和 map 的函数传参语义
1 | // 切片:append 在函数内不影响外部 |
11. 本课练习
练习 1:学生信息管理
要求:
- 创建
map[string]int,存储若干学生的成绩 - 实现添加学生、查询成绩、删除学生三个操作
- 用"逗号 ok"模式处理查不到的情况
练习 2:词频统计
要求:
- 给定一个字符串,统计每个字符出现的次数
- 例如
"hello"输出h:1 e:1 l:2 o:1
练习 3:用 map 去重
要求:
- 给定一个
[]string切片 - 用 map 去重,保持首次出现的顺序
- 返回去重后的切片
练习 4:map 作为函数参数
要求:
- 定义函数
update(m map[string]int, key string, delta int) - 如果 key 不存在就添加,存在就在原值基础上加 delta
- 验证函数内外共享同一个 map
练习 5:分组
要求:
- 给定一个
[]string,例如["apple", "ant", "bat", "bee", "cat"] - 按首字母分组,返回
map[string][]string - 例如
{"a": ["apple", "ant"], "b": ["bat", "bee"], "c": ["cat"]}
练习 6:按键排序遍历
要求:
- 创建一个
map[string]int - 实现按键的字母序排序后遍历打印
- 提示:先提取键到切片,排序切片,再遍历
12. 自测题
12.1 概念题
- map 的类型声明语法是什么?
- 怎么判断 map 中某个键是否存在?
- 对
nilmap 进行写入会怎样? - map 的遍历顺序是固定的吗?
- map 是值类型还是引用类型?传给函数后修改会影响外部吗?
delete函数做什么?对不存在的键使用会报错吗?map[string]int{"a": 0}中,m["b"]返回什么?怎么区分"值是 0"和"键不存在"?- 怎么用 map 实现集合(Set)?
13. 本课总结
这一课你学到了 Go 中最常用的查找型数据结构——map。
你现在应该已经理解:
- map 是键值对集合,用
map[K]V声明 - 用字面量或
make(map[K]V)创建 m[key] = val添加或修改,delete(m, key)删除- 用
val, ok := m[key]判断键是否存在 for range遍历 map,顺序不固定- map 是引用类型,传给函数后内外共享
nilmap 只能读不能写,使用前务必初始化- 不要在并发场景直接使用普通 map
14. 下一课预告
下一课我们深入 Go 的字符串底层:字符串、byte 与 rune。
会重点讲:
- 字符串在内存中是什么样子
byte和rune分别是什么- 为什么
len("你好")不等于 2 - 怎么正确遍历和处理中文字符串
- 字符串不可变性
学完下一课,你就能正确处理包含中文、emoji 等多字节字符的场景了。





