编程语言研究书库

map:键值对的字典,以及三个最容易咬人的细节

map:键值对的字典,以及三个最容易咬人的细节

这一章讲 Go 的字典:map。 它按「键」存「值」——像一本电话簿, 报人名、查号码,不用从头翻到尾。读完你会走通 map 的一生: 出生(make 或字面量)、查值(以及「查不到」的陷阱)、删键、遍历, 以及它和切片一样的引用本质。

1. map 是什么

书里给的定义:map 是以「键值对」存值的数据结构;键唯一、不能改, 键的类型可以是任何「可比较」的类型(可比较 = 能用 == 判等的类型: 数字、字符串、布尔、指针、结构体(字段都可比较时)等; 切片和 map 自己不能当键,因为它们不可比较)1。

别名顺带记一下:别的语言里它叫哈希(hash)或字典(dictionary)1。 用它图什么,书里列了四条:快查快取、按键直达、按键删除、实现哈希表2。

2. 主线例子:一部电话簿的完整一生

照原书的 phone_dict 走完全程(键是 string 人名,值是 int64 电话号)3。

出生——三种姿势。

var phone_dict map[string]int64   // 声明:这是 nil map!
phone_dict2 := map[string]int64{  // 字面量:带着数据出生
    "Rahul": 9876543201,
    "Swati": 9123456789,
}
phone_dict3 := make(map[string]int64)   // make:空但可用

细节一(本章第一个坑):nil map 只许读、不许写。 书里说得很直白:从 nil map 读,返回空结果;往 nil map 写,运行时直接报错4。 所以声明了 map 变量,要么用字面量、要么用 make 初始化,裸着用会崩—— 和切片不一样(nil 切片能 append,nil map 不能写)。

日常——增、查、改。

phone_dict2["aayush"] = 9823165748        // 增/改:键不存在就建,存在就覆盖
fmt.Println(phone_dict2["Swati"])          // → 9123456789

细节二(本章最重要的坑):查不存在的键,不报错,返回值的零值。

fmt.Println(phone_dict3["aayush"])   // → 0

对照数组:数组越界是错误,map 查无此键不是错误—— 它安安静静返回 int64 的零值 05。这意味着什么? 如果你的电话簿里真存了一个号码 0,或者你拿 0 当「没有」的信号, 程序会悄悄出错。解药是下面的「逗号 ok 模式」。

3. 查键:逗号 ok 模式与取出所有键

val, ok := m[key] 是检查键是否存在的标准姿势: 键在,ok = true,val 是对应的值;键不在,ok = false,val 是该类型的零值6:

val, ok := phone_dict3["Swati"]
// → true 9123456789

val, ok = phone_dict3["Suman"]
// → false 0

书里特意指出这和接口的类型断言是同一个模式(t, ok := i.(T))6—— Go 里「一个动作可能失败」的地方,都给你一对「结果 + 成没成」。 想要所有键?书里用了反射包的 MapKeys()(有点重); 日常更常用的是遍历(下节)时收集7。

4. 遍历:没有顺序,要有序先排序键

for key := range phone_dict3 {        // 只拿键
    fmt.Println(key)
}
for key, val := range phone_dict3 {   // 键和值都拿
    fmt.Println(key, val)
}

细节三(本章第三个坑):map 的遍历顺序是随机的。 书里明说:map 不保证任何顺序,每次运行的输出顺序都可能不同; 要按顺序处理,先把键收集进切片、排序,再按排好的键取值8:

var keys []string
for k := range phone_dict3 {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", phone_dict3[k])
}
// → Key: Deepak Value: 8000000000 / Key: Mayank …(按字母序)

这个「随机」是 Go 有意的设计:防止程序员无意中依赖某个顺序, 把偶然当必然(补充,不在书里,来自通用知识——顺序随机化的机制确为语言有意为之)。

5. 删除与比较

delete(m, key) 删键:不返回任何东西;键不存在也不报错,安安静静什么都不做9:

delete(phone_dict3, "Suman")
// Before: map[... Suman:9675463768 ...]
// After:  map[...]                    ← Suman 没了

比较:两个层面,规矩不同。

phone_dict == nil                        // 合法:只许和 nil 比
reflect.DeepEqual(m1, m2)                // 判全等:键值对逐对相同才 true

书里演示了 DeepEqual 的灵敏:改一个值(Swati 换成别的号), 两个原本相等的 map 立刻不等10。 map 不能直接用 == 互相比较(编译错),这点和切片一致。

6. 引用类型:赋值不等于复制

map 和切片一样是指向内部结构的引用。把一个 map 赋给另一个变量, 两边指向同一份数据;一边改,另一边看得见。传进函数也一样11:

  phone_dict["Mayank"]            → 9342516789
  phone_dict1 := phone_dict        // 复制的是「指针」,不是数据
  phone_dict1["Mayank"] = 1234567890
  phone_dict["Mayank"]            → 1234567890   ← 原 map 也被改了!

  传参同理:change_value(phone_dict)
    函数里 phone_dict1["Mayank"] = 1234567890
    出来后 phone_dict["Mayank"] 跟着变

图说:赋值/传参只复制了指向数据的门牌号,数据本身始终只有一份11。 (想要一份真正的副本?得手动建新 map 逐个搬——书里没讲这个惯用写法, 用到时记住方向即可。)

判断(我们的,不是书里的): map 的「引用语义 + 随机遍历 + 零值陷阱」 三件套,把它钉死在「好用但必须有纪律」的位置上: 查键必用逗号 ok;跨函数共享 map 前要意识到对方会改; 需要有序就用键切片排序。少了任何一条,bug 都是潜伏型而非当场爆。 如果错,会错在: Go 标准库(如 sync.Map、一些并发容器)在某些场景 提供了更合适的替代——但那是并发专题的事,单线程下原生 map 仍是默认答案。

7. 作者的判断与证据

这一章是规则与机制,没有推测。值得表扬的一处: 「nil map 读可以写报错」「查无键返回零值」这两个坑,书里都主动点破了, 没有让读者自己踩(不少入门书会漏)45。

8. 边界与局限

  • m[k] 的零值陷阱的完整解法就是逗号 ok;书里没有再讲 「值本身可能是零值」的二级场景(比如 map[string]int 里存了合法的 0);
  • 并发读写 map 会崩(fatal error: concurrent map read and map write) ——这是 Go 运行时的一条硬规则,本书直到第 16 章才间接涉及;
  • 书中取所有键用 reflect.ValueOf(m).MapKeys(),属于「杀鸡用牛刀」, 日常用 range 收集即可;
  • 复合键(map 的键是结构体)、map 嵌套的惯用写法未展开。

9. 可带走的

  1. map = 键值对字典;键唯一、键类型必须可比较(切片/map 不能当键);
  2. 出生三姿势:声明(nil)、字面量、make;nil map 只许读不许写;
  3. 查无此键返回零值、不报错——判断存在必须用 val, ok := m[key];
  4. len(m) 是键的数量;
  5. 遍历顺序随机;要顺序,先收集键、sort 排序、再取值;
  6. delete(m, k) 删键,键不存在也不报错;
  7. map 之间不能用 ==,判全等用 reflect.DeepEqual,判空用 == nil;
  8. map 是引用类型:赋值/传参共享同一份数据,函数里改外面看得见;
  9. 逗号 ok 模式是 Go 的通用失败信号(map、类型断言同款);
  10. 想让函数拥有自己的副本,手动建新 map 逐个拷贝。

10. 原文地图

主题原书章原文位置
map 定义与用途Mapstext/22-fm-learning-go-programming.txt:49(搜「key-value pair」) · text/22-fm-learning-go-programming.txt:58(搜「Fast lookups」)
声明与 nil 默认值Mapstext/22-fm-learning-go-programming.txt:76(搜「map [type_of_keys]」) · text/22-fm-learning-go-programming.txt:91(搜「nil, as it does not have any key」)
nil map 读可以写报错Mapstext/22-fm-learning-go-programming.txt:97(搜「attempting to write to a nil map」)
字面量与 make 初始化Mapstext/22-fm-learning-go-programming.txt:100(搜「map [string] int64{}」) · text/22-fm-learning-go-programming.txt:127(搜「make function is also used」)
查无键返回零值Mapstext/22-fm-learning-go-programming.txt:145(搜「does not belong to that map」) · text/22-fm-learning-go-programming.txt:217(搜「0」)
len 与逗号 ok 模式Mapstext/22-fm-learning-go-programming.txt:253(搜「length of phone_dict3」) · text/22-fm-learning-go-programming.txt:280(搜「type assertion in the interface」) · text/22-fm-learning-go-programming.txt:343(搜「false 0」)
MapKeys 取所有键Mapstext/22-fm-learning-go-programming.txt:313(搜「MapKeys」) · text/22-fm-learning-go-programming.txt:337(搜「Keys of phone_dict3」)
遍历无序与排序键Mapstext/22-fm-learning-go-programming.txt:349(搜「no order is specified」) · text/22-fm-learning-go-programming.txt:406(搜「sort.Strings」)
deleteMapstext/22-fm-learning-go-programming.txt:445(搜「does not return anything」) · text/22-fm-learning-go-programming.txt:499(搜「After deleting a key」)
DeepEqual 比较Mapstext/22-fm-learning-go-programming.txt:514(搜「same keys and the same key's associated values」) · text/22-fm-learning-go-programming.txt:574(搜「false true false」)
引用类型语义Mapstext/22-fm-learning-go-programming.txt:580(搜「refer to the same data structure」) · text/22-fm-learning-go-programming.txt:631(搜「1234567890」) · text/22-fm-learning-go-programming.txt:691(搜「After calling function」)

Footnotes

  1. 出处:「Maps」第 49 段(text/22-fm-learning-go-programming.txt:49,搜「key-value pair」)。「键可比较」的完整口径:切片、map、函数不可比较,不能当键(补充自 Go 语言规范)。 ↩ ↩2

  2. 出处:「Maps」第 55–67 段(text/22-fm-learning-go-programming.txt:58,搜「Fast lookups」)。 ↩

  3. 出处:「Maps」第 70–218 段(text/22-fm-learning-go-programming.txt:100,搜「map [string] int64{}」;text/22-fm-learning-go-programming.txt:121,搜「aayush」)。 ↩

  4. 出处:「Maps」第 97 段(text/22-fm-learning-go-programming.txt:97,搜「attempting to write to a nil map」)。 ↩ ↩2

  5. 出处:「Maps」第 145–217 段(text/22-fm-learning-go-programming.txt:145,搜「does not belong to that map」;text/22-fm-learning-go-programming.txt:217,搜「0」)。 ↩ ↩2

  6. 出处:「Maps」第 274–343 段(text/22-fm-learning-go-programming.txt:280,搜「type assertion in the interface」;text/22-fm-learning-go-programming.txt:340,搜「true 9123456789」;text/22-fm-learning-go-programming.txt:343,搜「false 0」)。 ↩ ↩2

  7. 出处:「Maps」第 301–337 段(text/22-fm-learning-go-programming.txt:313,搜「MapKeys」)。 ↩

  8. 出处:「Maps」第 346–439 段(text/22-fm-learning-go-programming.txt:349,搜「no order is specified」;text/22-fm-learning-go-programming.txt:424,搜「Rahul Swati Suman Mayank Deepak」;text/22-fm-learning-go-programming.txt:427,搜「Key: Deepak」)。 ↩

  9. 出处:「Maps」第 442–502 段(text/22-fm-learning-go-programming.txt:445,搜「does not return anything」;text/22-fm-learning-go-programming.txt:499,搜「After deleting a key」)。 ↩

  10. 出处:「Maps」第 505–574 段(text/22-fm-learning-go-programming.txt:514,搜「same keys and the same key's associated values」;text/22-fm-learning-go-programming.txt:574,搜「false true false」)。 ↩

  11. 出处:「Maps」第 577–691 段(text/22-fm-learning-go-programming.txt:580,搜「refer to the same data structure」;text/22-fm-learning-go-programming.txt:631,搜「1234567890」;text/22-fm-learning-go-programming.txt:691,搜「After calling function」)。 ↩ ↩2

On this page