本文档旨在引导你自主阅读 Go 1.21 的 map 源码

把下面内容当作阅读源码前的准备清单与思考题。

推荐阅读

核心结构与基础

在开始阅读代码前,请先思考并尝试在源码中找到以下问题的答案:

重点操作:查找、插入与扩容

这是 map 最核心的逻辑,位于 src/runtime/map.go。阅读时请关注:

  • 查找过程(mapaccess)
    • 哈希值是如何被拆分使用的?(低位用来做什么?高位 tophash 用来做什么?)
    • 如果 map 正在扩容期间,查找逻辑会有什么特殊处理?(如何去 oldbuckets 中找数据?)
  • 插入过程(mapassign)
    • 插入新 key 时,如果当前 bucket 已经满了(8个槽位都占用了),会发生什么?
  • 扩容机制(hashGrow 与 evacuate)
    • 触发条件:触发扩容的两个核心条件是什么?(装载因子 loadFactor > 6.5,或者溢出桶太多)
    • 扩容类型:什么是“翻倍扩容”?什么是“等量扩容”(sameSizeGrow)?等量扩容是为了解决什么极端场景?
    • 渐进式迁移(evacuate):为什么 Go 选择在每次插入或删除时搬迁 1~2 个 bucket,而不是一次性搬迁完?(思考对性能抖动的影响)

进阶思考

  • 遍历的无序性(mapiterinit)
    • 为什么在 Go 中 for range 遍历 map 每次的顺序都不一样?
    • 尝试在源码中寻找随机数生成的逻辑(fastrand),看看它是如何决定遍历的起始 bucket 和槽位偏移量的。
  • 并发安全(flags 字段)
    • 为什么说 Go 的原生 map 不是并发安全的?
    • 源码中是如何检测并抛出 concurrent map read and map write panic 的?(关注 hmap.flags 中的 hashWriting 标志位)。
  • 内存回收与泄露陷阱
    • 执行 delete(m, key) 删除元素后,map 占用的内存会立即归还给操作系统吗?
    • 如果一个 map 曾经存了海量数据,后来删除了绝大部分,它的 bucket 数量会缩小吗?如果不会,应该如何优化以释放内存?
  • Key 的类型限制
    • 什么样的类型可以作为 map 的 key?(为什么 slice、map、func 不行?)
    • 底层是如何比较两个 key 是否相等的?(关注 alg.equal)。