跳到主要内容

3.2 map 映射

本节目标:掌握 map 的增删改查、comma ok 判断键存在、遍历,以及几个必知的坑。

一、为什么需要 map

上一节用切片存了全班成绩。新需求来了:根据名字查成绩。用切片只能一个个找:

names := []string{"张三", "李四", "王五"}
scores := []int{90, 85, 78}

// 查"李四"的分数?循环遍历 names 找下标,再去 scores 取……
// 两个切片还得时刻保持对齐,插一个人全乱

我们想要的是按名字直接取。这就是 map(映射)——键值对集合,类似 Python 的 dict、Java 的 HashMap:

"张三" → 90
"李四" → 85 这就是一个 map[string]int
"王五" → 78

二、创建 map

// 方式 1:make 创建空 map
scores := make(map[string]int)

拆一下类型:

map[string]int
↑ ↑
键类型 值类型 —— "用 string 查 int"
// 方式 2:字面量创建并初始化
scores := map[string]int{
"张三": 90,
"李四": 85, // ⚠️ 最后一项后面的逗号必须有!(换行写时)
}

⚠️ 本节头号坑:nil map 不能写入。

var m map[string]int // 只声明不初始化 → m 是 nil
m["a"] = 1 // ❌ panic: assignment to entry in nil map

上一节说 nil 切片可以直接 append,map 没有这个待遇——nil map 只能读(读到零值),一写就崩。规则背下来:map 用前必须 make 或字面量初始化

三、增删改查

scores := make(map[string]int)

// 增 / 改是同一个语法:键不存在就新增,存在就覆盖
scores["张三"] = 90
scores["李四"] = 85
scores["张三"] = 95 // 覆盖成 95

// 查
fmt.Println(scores["张三"]) // 95

// 删:内置 delete 函数(删不存在的键不报错,静默无事)
delete(scores, "李四")

// 数量
fmt.Println(len(scores)) // 1

四、重点:查不存在的键不报错!

这是 map 最重要的知识点,Python 转来的尤其注意(Python 是抛 KeyError,Go 完全相反):

scores := map[string]int{"张三": 90}

fmt.Println(scores["路人甲"]) // 0 —— 不报错!返回值类型的零值

于是有个经典歧义:拿到 0,到底是"考了 0 分"还是"查无此人"?

comma ok:一次取值,两个答案

map 取值其实可以接两个返回值,第二个是"键存不存在":

score, ok := scores["张三"]
if ok {
fmt.Println("找到了,分数是", score)
} else {
fmt.Println("查无此人")
}

配合 2.4 学的 if 初始化语句,Go 程序员的惯用姿势是:

if score, ok := scores["张三"]; ok {
fmt.Println("分数是", score)
}

这个写法叫 comma ok,是 Go 的招牌习语之一。什么时候用:"键不存在"和"值恰好是零值"需要区分时就用它;无所谓时(比如计数器)直接取。

五、遍历 map

还是 for range,两个变量是键、值

scores := map[string]int{"张三": 90, "李四": 85, "王五": 78}

for name, score := range scores {
fmt.Println(name, score)
}

for name := range scores { // 只要键
fmt.Println(name)
}

⚠️ 遍历顺序是随机的!

跑三次上面的代码,可能得到三种顺序。这不是 bug——Go 故意每次打乱,就是防止你写出依赖顺序的代码(有些语言的 map 碰巧有序,换个版本就炸)。

要按顺序输出?老老实实先把键收集起来排序

import "slices"

keys := make([]string, 0, len(scores))
for k := range scores {
keys = append(keys, k) // 上一节的"收集"套路
}
slices.Sort(keys)

for _, k := range keys {
fmt.Println(k, scores[k]) // 按键的字典序输出
}

这三步(收集键 → 排序 → 按序取值)是 Go 的固定套路,直接背。

六、值可以是任何类型

map 的值类型不限,组合出各种实用结构:

// 值是切片:一个人多个爱好
hobbies := map[string][]string{
"张三": {"篮球", "编程"},
"李四": {"阅读"},
}
hobbies["张三"] = append(hobbies["张三"], "游泳")

// 值是 map:嵌套
users := map[string]map[string]string{
"u1001": {"name": "张三", "city": "北京"},
}
fmt.Println(users["u1001"]["name"]) // 张三

// 值是结构体 —— 实际项目最常见的形态,下一节正式登场

键的限制倒是有一条:键必须是可比较的类型。string、int、bool、数组都行;切片、map、函数不能当键(它们连 == 都不支持),编译报错 invalid map key type

七、经典应用:计数器

map 最高频的实战用法——统计出现次数:

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

counter := make(map[string]int)
for _, w := range words {
counter[w]++ // 一行搞定!
}

fmt.Println(counter) // map[go:3 java:1 python:2]

counter[w]++ 为什么第一次遇到 "go" 时不出错?拆开看:counter["go"] 不存在 → 返回零值 0 → 0+1=1 存回去。"不存在返回零值"在这里从坑变成了神助攻——不需要像别的语言那样先判断"键在不在,不在先设 0"。

八、map 也是"共享"的

和切片一样,map 赋值/传参不复制数据

m1 := map[string]int{"a": 1}
m2 := m1 // 指向同一份数据
m2["a"] = 100
fmt.Println(m1) // map[a:100] —— m1 也变了

把 map 传进函数,函数内的修改外面全看得见。这个特性下一节讲结构体、3.4 讲指针时会串成完整的图景。

本节报错/怪象速查表

现象人话翻译
panic: assignment to entry in nil map对没 make 的 map 写入,先 make(map[K]V)
查不存在的键得到 0 / "" / false不是 bug,返回零值;要区分用 v, ok := m[k]
每次运行遍历顺序不一样故意的;要有序就收集键排序再遍历
invalid operation: m1 == m2map 之间不能比较,只能和 nil 比
invalid map key type []int切片/map/函数不能当键
missing ',' before newline字面量换行写时,最后一项后面漏了逗号

练习

1. 动手:创建 map 存 3 个朋友的生日(姓名 → 日期字符串),依次:新增一人、修改一人、删除一人、用 comma ok 查询一个不存在的人。

2. 动手:统计 "the quick brown fox jumps over the lazy dog the fox" 中每个单词的出现次数(提示:strings.Fields(s) 按空格拆成切片),并按单词字典序打印。

3. 猜输出:先别运行,猜猜打印什么?

m := map[string]int{"a": 1}
fmt.Println(m["b"])

v, ok := m["b"]
fmt.Println(v, ok)

m["b"]++
fmt.Println(m["b"])

4. 修 bug:下面的程序会 panic,找出原因修好它:

func main() {
var stock map[string]int
stock["苹果"] = 10
stock["香蕉"] = 5
fmt.Println(stock)
}
点击查看答案
// 1
birthdays := map[string]string{
"张三": "2000-01-01",
"李四": "1999-05-20",
"王五": "2001-12-31",
}
birthdays["赵六"] = "1998-08-08" // 增
birthdays["张三"] = "2000-02-02" // 改
delete(birthdays, "王五") // 删
if b, ok := birthdays["神秘人"]; ok { // 查
fmt.Println(b)
} else {
fmt.Println("查无此人")
}

// 2
import (
"fmt"
"slices"
"strings"
)

text := "the quick brown fox jumps over the lazy dog the fox"
counter := make(map[string]int)
for _, w := range strings.Fields(text) {
counter[w]++
}

keys := make([]string, 0, len(counter))
for k := range counter {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
fmt.Println(k, counter[k])
}

3. 输出:

0 ← 不存在的键返回零值
0 false ← comma ok:值是零值,ok 明确告诉你"不存在"
1 ← 零值 0 加 1 存回去,计数器原理

4. var stock map[string]int 声明的是 nil map,写入即 panic。改成:

stock := make(map[string]int) // 或 stock := map[string]int{}

本节小结

  • map 是键值对:map[键类型]值类型用前必须 make 或字面量初始化,nil map 一写就 panic
  • 增/改同一个语法,删用 delete(m, k)
  • 查不存在的键返回零值不报错;要区分就用 comma ok:if v, ok := m[k]; ok
  • 遍历顺序随机是故意的;要有序:收集键 → 排序 → 按序取
  • 计数器 counter[w]++ 是 map 第一名的实战套路
  • map 赋值/传参共享数据;切片、map、函数不能当键

下一节:3.3 结构体