讲解

map 是 Go 内置的哈希表,存键值对:map[string]int 表示字符串键、整数值。创建用字面量或 make,写入和读取都用方括号:m["键"] = 值、m["键"]。键类型必须支持 == 比较(字符串、数字、布尔、不含切片的结构体等),切片和 map 不能当键。

读取不存在的键不会报错,而是返回值类型的零值——m["不存在"] 对 int 值返回 0。那怎么区分「键不存在」和「键存在但值恰好是 0」?用两个返回值的形式:v, ok := m["键"],ok 是布尔值表示键是否存在,这就是著名的「comma ok」惯用法,后面类型断言还会见到同款。

删除用内建函数 delete(m, key),删不存在的键也是安全的空操作。遍历用 for k, v := range m,但要牢记:map 的遍历顺序是故意随机的,每次运行都可能不同——Go 刻意这么设计,防止任何人依赖哈希表的内部顺序。需要有序输出就把键收集到切片里排序后再遍历。

示例

map 的增删查与 comma ok:

package main

import "fmt"

func main() {
	ages := map[string]int{"小明": 20, "小红": 22}
	ages["小刚"] = 25         // 写入
	fmt.Println(ages["小明"]) // 读取: 20

	v, ok := ages["不存在"]
	fmt.Println("查不存在的键:", v, ok) // 0 false

	delete(ages, "小红")
	fmt.Println("删除后长度:", len(ages))
}

经典应用:统计单词出现次数:

package main

import (
	"fmt"
	"strings"
)

func main() {
	text := "go is simple and go is fast"
	counts := make(map[string]int)
	for _, w := range strings.Fields(text) {
		counts[w]++ // 不存在的键自动按零值自增,无需特判
	}
	fmt.Println(counts)
}

遍历 map,并用排序后的键切片保证输出顺序稳定:

package main

import (
	"fmt"
	"slices"
)

func main() {
	prices := map[string]float64{"苹果": 3.5, "香蕉": 2.0, "橙子": 4.0}

	keys := make([]string, 0, len(prices))
	for name := range prices {
		keys = append(keys, name)
	}
	slices.Sort(keys)

	total := 0.0
	for _, name := range keys { // 按键排序后遍历,顺序稳定
		fmt.Printf("%s: %.1f 元\n", name, prices[name])
		total += prices[name]
	}
	fmt.Printf("合计: %.1f 元\n", total)
}

常见坑

  • 对 nil map 写入:var m map[string]int 得到 nil map,读它返回零值,写它直接 panic。写入前必须 make 或字面量初始化——「nil map 能读不能写」要背下来。
  • 依赖遍历顺序:map 遍历顺序随机且每次不同,把 range map 的结果直接输出给用户或写进测试断言都会 flaky。要顺序就先排序键。
  • 以为取不存在的键会报错:返回零值而不是异常,count["x"]++ 能跑是因为这个特性,但反过来「键不存在却以为有值」的 bug 也来了。重要判断用 comma ok。
  • 试图取 map 值的地址:m["key"].Field = x 编译不过,因为 map 扩容会搬移元素,地址不稳定。改结构体字段要先取出整个值、改完再塞回去,或干脆存指针。
  • 并发读写裸 map:map 不是并发安全的,多 goroutine 同时读写会触发 fatal error。并发场景用互斥锁保护或 sync.Map,并发章节会展开。

小结

map 存键值对,读缺失键得零值,comma ok 区分存在性,delete 删除,遍历顺序随机。下一节学习把多个字段捆成一个整体:结构体。