首页 / Go 语言入门教程 / map 进阶

Go 语言入门教程

map 进阶

本教程共 80 篇 · 第 33 篇 · 更新于 2026-07-27 · 约 7 分钟阅读

GoGo 入门教程map遍历嵌套逗号ok键值

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 包的常用函数。