map 进阶
本教程共 80 篇 · 第 33 篇 · 更新于 2026-07-27 · 约 7 分钟阅读
33. map 进阶
本节目标:搞懂 map 遍历为什么无序,学会处理 key 不存在的情况,掌握嵌套 map 的使用方式。
遍历是无序的
用 for range 遍历 map,每次的顺序可能都不一样:
package main
import "fmt"
func main() {
m := map[string]int{
"apple": 5,
"banana": 3,
"cherry": 8,
"date": 2,
}
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
}
你多跑几次就会发现,输出的顺序每次都不一样。这是 Go 故意的,目的是不让你依赖遍历顺序。如果 map 的遍历顺序固定,你可能会写出依赖这个顺序的代码,一旦 Go 内部实现变了,你的代码就会出 bug。
需要有序遍历怎么办
如果你确实需要按顺序输出,先取出 key 排序,再按排序后的 key 遍历:
package main
import (
"fmt"
"slices"
)
func main() {
m := map[string]int{
"apple": 5,
"banana": 3,
"cherry": 8,
"date": 2,
}
// 取出所有 key 并排序
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
// 按排序后的 key 遍历
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
}
输出:
apple: 5
banana: 3
cherry: 8
date: 2
Tip从 Go 1.21 开始,排序可以直接用
slices.Sort。Go 1.21 之前需要用sort.Strings。
逗号 ok 模式详解
上一章提到过,取 map 值时用 v, ok := m[key] 能判断 key 是否存在。这里展开讲讲为什么重要。
假设你有一个 map 记录学生成绩,某个学生缺考了成绩记为 0:
scores := map[string]int{
"Alice": 90,
"Bob": 0, // Bob 考了但得0分
}
// 光看值,分不清 Bob 是考了0分还是没参加
fmt.Println(scores["Bob"]) // 0
fmt.Println(scores["Charlie"]) // 0 Charlie 根本不在 map 里
// 用逗号 ok 就清楚了
v, ok := scores["Bob"]
fmt.Println(v, ok) // 0 true Bob在,值是0
v, ok = scores["Charlie"]
fmt.Println(v, ok) // 0 false Charlie不在
Warning写库或框架时,千万别用「值是否为零值」来判断 key 是否存在。一定用逗号 ok 模式,否则零值和不存在会混淆。
map 嵌套
实际开发中经常遇到嵌套的 map,比如 map[string]map[string]int,可以理解成「每个城市对应一个商品价格表」。
创建嵌套 map
package main
import "fmt"
func main() {
// 外层 map: 城市 -> 内层 map
cities := make(map[string]map[string]int)
// 必须先初始化内层 map
cities["北京"] = make(map[string]int)
cities["北京"]["苹果"] = 5
cities["北京"]["香蕉"] = 3
cities["上海"] = make(map[string]int)
cities["上海"]["苹果"] = 6
cities["上海"]["香蕉"] = 4
fmt.Println(cities["北京"]["苹果"]) // 5
fmt.Println(cities["上海"]["香蕉"]) // 4
}
Warning嵌套 map 最常见的坑就是忘了初始化内层 map。下面这样写会 panic:
cities := make(map[string]map[string]int)
cities["北京"]["苹果"] = 5 // panic! 内层map是nil
外层 map 用 make 初始化了,但内层 map 还是 nil。必须先 cities["北京"] = make(map[string]int) 才能写入。
遍历嵌套 map
双重循环就行:
for city, prices := range cities {
fmt.Printf("%s:\n", city)
for item, price := range prices {
fmt.Printf(" %s: %d\n", item, price)
}
}
key 类型的选择
上一章说了 map 的 key 必须可比较。实际选择时有几个建议:
用字符串做 key 最常见,适合表示名字、ID、URL 等文本标识。
用整数做 key 性能略好,适合表示索引、编号等数字标识。
用结构体做 key 适合复合键。比如用「日期+商品名」做 key:
type Key struct {
Date string
Item string
}
sales := make(map[Key]int)
sales[Key{"2026-01-01", "apple"}] = 100
sales[Key{"2026-01-01", "banana"}] = 50
fmt.Println(sales[Key{"2026-01-01", "apple"}]) // 100
Note结构体做 key 时,所有字段都必须是可比较的类型。不能包含切片、map 或函数。
并发安全
map 不是并发安全的。多个 goroutine 同时读写同一个 map 会 panic:
// 这样会出问题
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m[i]
}
}()
并发场景下用 sync.Map(后面并发章节会讲),或者自己加锁。
小结
map 进阶记住这几点:
- 遍历顺序随机,需要有序就先排序 key
- 用逗号 ok 模式判断 key 是否存在,别靠零值
- 嵌套 map 要逐层初始化
- 并发读写 map 会 panic,后面学 sync 包解决
下一章讲字符串和字节切片的转换,以及 strings 和 strconv 包的常用函数。