Go Map 映射 — 键值对与增删改查
Map(映射)就是其它语言里的"字典"或"哈希表"——一组无序的键值对,通过键(key)快速找到值(value)。它是 Go 里除了切片之外最常用的数据结构,凡是需要"按某个标识查数据"的场景,比如用户信息、配置项、计数统计,几乎都会用到 map。这一篇我们从创建到遍历一次讲清。
一、什么是 map
map 是引用类型,底层由哈希表实现。它有两条铁律需要先记住:
- 键必须可比较。可以用字符串、数字、布尔、数组、指针、结构体当键,但不能用切片、map、函数当键——它们的值不能判等,编译器会直接拒绝。
- 遍历顺序是随机的。Go 故意把遍历顺序打乱,防止开发者依赖哈希表的实现细节。需要有序输出时,先把 key 取出来排序,再按顺序查 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.Strings 或 sort.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加读写锁,读时 RLock、写时 Lock。 - 高并发或 key 相对稳定:直接用
sync.Map,它内置并发控制,读多写少场景性能很好,但 API 稍繁琐(Store/Load/Delete)。
大多数业务场景下,用一把 sync.RWMutex 配普通 map 就够了,理解起来也最直观。
小结
- map 是无序键值对、引用类型,key 必须可比较。
- nil map 能读不能写,写之前记得
make或字面量初始化。 - 两值写法判断 key 是否存在,是 Go map 的核心技巧。
- 遍历顺序随机;并发场景必须加锁或用
sync.Map。
下一篇 Go 结构体 Struct 讲如何把多个字段组合成一个对象。上一篇 Go 数组与切片。