Go map 入门:键值查找、ok 判断与集合写法
上一课我们已经学会了用数组和切片按顺序保存一组数据。接下来要解决的新问题是:如果我不是想按位置找第 1 个、第 2 个元素,而是想用“名字 → 分数”“单词 → 次数”这种方式快速查找,该怎么做? 这就是 map 要解决的事情。
这一课只聚焦 syllabus 中的三个重点:学会创建和遍历 map,理解为什么读取 map 时常常要配合 ok 模式判断键是否存在,以及认识 map 还能怎样表示“集合”。最后我们也会先建立一个重要直觉:普通 map 不能在多个 goroutine 中同时随意读写。
学习目标
- 理解
map是“键和值”的对应关系,学会创建、读取、修改和删除元素 - 掌握遍历
map的基本写法,知道遍历顺序默认不固定 - 理解零值读取与
value, ok := map[key]的区别 - 学会用
map[T]bool或map[T]struct{}表示集合,并知道普通map不适合并发读写
核心概念
1. map 是什么
map 可以理解成一个“根据键快速找到值”的表。
例如:
scores := map[string]int{
"小李": 95,
"小王": 88,
}这里:
- 键(key)是
string - 值(value)是
int "小李"对应95"小王"对应88
和切片不同,map 不是靠下标 0、1、2 查找,而是靠键来找值。
2. 如何创建 map
最常见的创建方式有两种。
写法一:直接用字面量创建
ages := map[string]int{
"Alice": 20,
"Bob": 22,
}这适合一开始就有数据的场景。
写法二:先用 make 创建,再逐个放入
prices := make(map[string]float64)
prices["苹果"] = 3.5
prices["香蕉"] = 4.2这适合“先准备一个空 map,之后再慢慢往里放数据”的情况。
零基础阶段先记住一个很重要的规则:
如果
map还是nil,就不能直接写入键值;通常先用make再赋值。
3. 读取、修改和删除元素
读取元素时,直接用键:
score := scores["小李"]修改元素也很直接:
scores["小李"] = 98如果键原本不存在,这种赋值也会自动变成“新增一个键值对”:
scores["小张"] = 91删除元素用 delete:
delete(scores, "小王")delete 的特点是:即使这个键不存在,也不会报错。它会安静地结束。
4. 为什么读取 map 时常配合 ok 模式
这是今天最关键的概念之一。
看下面这行:
score := scores["小赵"]如果 "小赵" 这个键不存在,会发生什么?Go 不会报错,而是返回值类型的零值。
比如:
map[string]int不存在的键会得到0map[string]string不存在的键会得到""map[string]bool不存在的键会得到false
问题在于:你有时分不清这个 0 到底表示“真实存的就是 0”,还是“其实这个键根本不存在”。
所以 Go 很常见的写法是:
score, ok := scores["小赵"]这里:
score是取出来的值ok是一个布尔值,表示这个键是否真的存在
例如:
score, ok := scores["小赵"]
if ok {
fmt.Println("找到了分数:", score)
} else {
fmt.Println("没有这个学员")
}你可以把它理解成:先取值,再顺手问一句“这个键真的在吗?”
5. 如何遍历 map
遍历 map 用 for range:
for name, score := range scores {
fmt.Println(name, score)
}如果你只关心键,也可以这样写:
for name := range scores {
fmt.Println(name)
}这里要特别记住一个初学者很容易忽略的点:
map的遍历顺序默认不固定,不要依赖它每次都按同样顺序输出。
如果你以后需要稳定顺序,通常要先把键取出来放到切片,再排序处理。但零基础阶段先知道“别假设顺序固定”就够了。
6. map 也可以表示集合
很多时候我们并不关心“值是什么”,只关心“某个元素在不在”。这时候 map 就可以拿来表示集合。
例如,用 map[string]bool 表示你已经学过哪些主题:
learned := map[string]bool{
"变量": true,
"函数": true,
"切片": true,
}判断某个主题是否学过:
if learned["map"] {
fmt.Println("已经学过 map")
}也可以写成更节省空间的形式:
visited := map[string]struct{}{
"beijing": {},
"shanghai": {},
}对于零基础来说,先把 map[string]bool 当成集合写法就够用了,因为更直观。
7. 为什么说普通 map 不适合并发读写
今天不展开 goroutine,但要先建立一个正确习惯:
普通
map不是并发安全的,不能在多个 goroutine 中同时随意写入,甚至“边写边读”也可能出问题。
这意味着:
- 在当前这种单线程入门程序里,直接用
map没问题 - 等以后学到并发时,如果多个 goroutine 共享同一个
map,要额外用锁或其他同步方式保护它
零基础阶段不用记所有并发细节,只要先避免形成“map 天然线程安全”的误解。
代码示例
下面这个程序把“创建 map”“读取与修改”“ok 模式”“遍历”“集合写法”放在一起。你可以先在空目录执行:
go mod init lesson06然后创建 main.go,写入以下内容,再运行 go run .:
package main
import "fmt"
func main() {
scores := make(map[string]int)
scores["小李"] = 95
scores["小王"] = 88
scores["小张"] = 0
fmt.Println("初始 scores:", scores)
scores["小王"] = 90
fmt.Println("修改后小王的分数:", scores["小王"])
if score, ok := scores["小李"]; ok {
fmt.Println("找到小李的分数:", score)
}
if score, ok := scores["小赵"]; ok {
fmt.Println("找到小赵的分数:", score)
} else {
fmt.Println("没有找到小赵,score 的零值是:", score)
}
fmt.Println("---- 遍历 scores ----")
for name, score := range scores {
fmt.Println(name, score)
}
delete(scores, "小张")
fmt.Println("删除小张后:", scores)
learned := map[string]bool{
"变量": true,
"函数": true,
"切片": true,
}
topic := "map"
if learned[topic] {
fmt.Println(topic, "已经学过")
} else {
fmt.Println(topic, "还没学过,今天开始学习")
}
learned[topic] = true
fmt.Println("更新后的 learned:", learned)
}如果运行成功,你应该重点观察四件事:
scores["小王"] = 90说明map中的值可以直接更新score, ok := scores["小赵"]能区分“键不存在”和“值刚好等于零值”for range可以遍历整个map,但输出顺序不保证固定learned这个map[string]bool就是一种很常见的“集合”写法
练习
- 创建一个
map[string]string,保存 3 个英文单词和它们的中文含义,并打印其中一个单词的解释。 - 定义一个
map[string]int保存三位同学的分数,其中一位分数设为0,再用value, ok := ...区分“键存在且值为 0”和“键不存在”。 - 写一个程序,向
map[string]int中加入几本书的库存数量,然后删除其中一本,观察结果。 - 用
map[string]bool表示“今天已经完成的练习题编号”或“已经学过的 Go 主题”,再写判断某个元素是否存在的代码。 - 用自己的话解释:为什么只看
scores["某人"]的结果,有时不足以判断这个键到底在不在?
小结
今天你已经学会了 Go 里非常常用的一种数据结构:map。你知道了它适合做“键 → 值”的快速查找,能用 make 创建、用 delete 删除、用 for range 遍历;更重要的是,你理解了 value, ok := map[key] 这组写法为什么重要,也认识到 map 还能拿来表示集合。最后请记住一个提前埋下的安全习惯:普通 map 不适合并发读写。 下一课我们会继续学习结构体与方法,让程序不仅能保存零散数据,还能把同一类数据组织成更清晰的类型。