Go Map 映射 — 键值对与增删改查

Map(映射)就是其它语言里的"字典"或"哈希表"——一组无序的键值对,通过键(key)快速找到值(value)。它是 Go 里除了切片之外最常用的数据结构,凡是需要"按某个标识查数据"的场景,比如用户信息、配置项、计数统计,几乎都会用到 map。这一篇我们从创建到遍历一次讲清。

一、什么是 map

map 是引用类型,底层由哈希表实现。它有两条铁律需要先记住:

二、声明与初始化

创建 map 主要两种方式。第一种是字面量 map[string]int{"a": 1},适合初始化时就已知内容;第二种是 make(map[string]int),得到一个空 map,之后再慢慢填充。

有一个非常容易踩的坑:var m map[string]int 声明后得到的是 nil map,不能直接往里写值——会 panic。要写值就必须先用 make 或字面量初始化。nil map 可以读(返回零值),但不能写,这是和 nil 切片不一样的地方。

三、增、删、改、查

map 的增改查删都是一句话的事,核心是两值写法判断 key 是否存在——这是 Go map 用得最多的技巧。

package main

import "fmt"

func main() {
    // 两种创建方式
    m := map[string]int{"a": 1, "b": 2}   // 字面量
    m2 := make(map[string]int)            // make,得到空 map

    // 增 / 改:直接赋值
    m["c"] = 3      // 新增
    m["a"] = 100    // 修改已有的 key

    // 查:两值写法,第二个返回值 ok 表示 key 是否存在
    v, ok := m["a"]
    fmt.Println(v, ok)    // 100 true
    v2, ok2 := m["x"]
    fmt.Println(v2, ok2)  // 0 false  (零值 + false)

    // 删:delete(map, key)
    delete(m, "b")
    fmt.Println(m)        // map[a:100 c:3]
    fmt.Println(len(m), m2)
}

几个要点慢慢看:增和改都用 m[key] = value,key 不存在就新增、存在就覆盖。有两种写法,单值 v := m["a"] 在 key 不存在时返回值类型的零值(int 是 0、string 是空串),你分不清到底是真有这个 key 还是值本来就是 0;所以更稳的是两值写法 v, ok := m["a"],ok 是布尔,明确告诉你 key 在不在。用内置函数 delete(m, key),删不存在的 key 也不会报错,直接什么都不发生。

四、遍历与 len

遍历 map 用 for k, v := range m,和切片一样的语法。但务必记住遍历顺序是随机的,同一段代码每次运行结果顺序都可能不同。len(m) 返回键值对的数量。

package main

import "fmt"

func main() {
    prices := map[string]int{"苹果": 5, "香蕉": 3, "橘子": 7}

    // 遍历:for + range,顺序是随机的!
    for k, v := range prices {
        fmt.Printf("%s -> %d 元\n", k, v)
    }

    // len:键值对的数量
    fmt.Println("共", len(prices), "种水果")
}

如果确实需要按固定顺序处理,标准做法是:先用一个切片把所有 key 收集起来,用 sort.Stringssort.Ints 排序,再按排序后的 key 逐个去 map 里取值。这样既保留了 map 的查询效率,又保证了输出有序。

五、经典用法:单词计数与嵌套 map

map 最经典的应用是"计数"——遇到一个元素就让对应 key 加 1。由于不存在的 key 读出来是零值,可以放心地直接 counts[w]++,不需要先判断存在不存在。下面同时演示了嵌套 map,即"值本身又是一个 map"。

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 经典案例:统计一段文字里每个单词出现几次
    text := "the quick brown fox the lazy dog the"
    counts := map[string]int{}
    for _, w := range strings.Fields(text) {
        counts[w]++           // 不存在时返回零值 0,直接 ++ 即可
    }
    fmt.Println(counts)
    // map[brown:1 dog:1 fox:1 lazy:1 quick:1 the:3]

    // 嵌套 map:值本身又是一个 map
    class := map[string]map[string]int{
        "一班": {"小明": 90, "小红": 85},
        "二班": {"小刚": 78},
    }
    fmt.Println(class["一班"]["小明"])   // 90
}

嵌套 map 用起来要注意:外层 map 的值必须先初始化,直接写 class["三班"]["小王"] = 80 会 panic,因为 class["三班"] 还是 nil。正确做法是先 class["三班"] = map[string]int{} 再赋值。日常开发里,如果嵌套层数多,很多人会改成 map[string]Student(用结构体当值)以简化代码。

六、并发安全:sync.Map 与 sync.RWMutex

最后一个必须知道的点:标准 map 不是并发安全的。多个 goroutine 同时读写同一个 map 会直接 panic(fatal error: concurrent map writes)。两种解决思路:

大多数业务场景下,用一把 sync.RWMutex 配普通 map 就够了,理解起来也最直观。

小结

下一篇 Go 结构体 Struct 讲如何把多个字段组合成一个对象。上一篇 Go 数组与切片

✈️💬