[{"content":"\t技术革命带来了什么？又带走了什么？\n![AI智能助手.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20260130233107348.png)\n最近从实习离职到秋招结束，开启了很长的一段空档期，这段时间写写论文、打打游戏、看看书。因为前司的 AI 工具之拉跨，导致与世隔绝了几个月。而最近一段时间的 Agent 使用之旅，让我有了一些感想。身为技术人员，Vibe Coding 到底是提高了程序员的能力，还是程序员需要提高自己的能力来迎接Vibe Coding呢？\n## Vibe Coding 的爽感\n从 GPT3 的 ChatBot 形式开始，到现在的 Curosr、Claude Code 以及 Kimi CLI 等形式，不得不说学习以及编程的门槛是真的在下降，可能再过一段时间，Agent 真是 is all you need 了。从在几年前我的剪切板里复制的是一段一段的代码，到现在复制的可能是一大段精心修改的 Prompt，防止因为自己的一个不小心而弄丢提示词。\n我现在将我观察到的特点总结为以下内容：\n1. **搜索廉价：** AI 代替了一部分浏览器的功能。在 GPT3 刚出的时候，常用的人都会发现，它有很严重的幻觉问题，当然，LLM 厂商也发现了这个问题。所以减少幻觉变成了一个研究点。不得不承认的是，虽然到现在为止幻觉问题还没有被完全攻破，但随着模型能调用一些 MCP 工具，或者使用 RAG 等进行增强等，模型吐出来的内容的可靠性不断增加，从而使得我们可以在 ChatBot 中查资料。而 AI 帮你搜索的好处是按照你的问题，AI 随搜索出答案，并且帮你总结聚合，**变向增大了你的带宽**。\n2. **学习成本降低：** 搜索廉价为我带来的第一个益处就是学习成本降低。以往学习过程中，某些具有一定学习成本的技能，在 AI 时代这个壁垒被打破了。例如，当我需要实现一个组件来完成我的定制化需求的时候，可能这个组件只占我系统的 5% 甚至更少，而且在未来可能用到这技能的时间也很少。但因为壁垒，以往我可能需要花费大量的时间在这里，这可能也是一种 **二八定律** 吧。但是 AI 时代的到来，大量数据的投喂，使得 LLM 几乎不可能不知道你所知道的内容（除了近期他没训练到的数据），而这时，AI 完全有能力帮你减少开发成本。（**当然不鼓励不学习新东西，而是这时候需要我们有一双慧眼**）\n3. **idea 廉价但仍有价值：** 范式的突破往往不是固步自封得来的。为什么这么说呢？LLM 有一点就是他能关联不同领域的内容，因为其训练数据之庞大，几乎覆盖所有领域，所以在对话过程中，很容易从其他领域得到启发，进而构成 idea。很多时候，想要打破一个范式，只了解一个领域的知识是远远不够的，比如说扩散模型我没记错的话，是受热力学中的扩散过程启发而来。虽然模型大部分时候提出的 idea 都是不成功的，但是**一千个中有一个能用的，一万个中有一个还不错的，一亿个中有一个闪亮的，是不是也能推动社会的发展？**\n## 细思极恐（\\*^\\*）\n上面讲解了一我观察到的大模型的能力，而随着这几年不断用 AI，我也发现了一些值得反思的内容。\n1. **肉体的怠惰：** 停止动手。这个是我有一段时间使用 AI 编程发现的，传统我们可能会去网上找一些代码片段拿来直接用（面向 CV 编程），而有 AI 我相信大家基本上都是让 AI 写。有一天我让 AI 写代码，我来来回回对话了不知多少轮，写出来的程序依旧有 Bug，后来我放弃了（指换了个 AI🤣）。最后发现导致代码 Bug 的只是一小块内容，换我自己查 Bug 可能十来分钟就查出来了，但是 AI 查了不知道多久，而以前写代码，我可能找到一个 Demo 如果用不了，会主动去 Demo 上改。这里就体现出了，**过度依赖 AI，可能会浪费一定时间。但是，有时候 AI 又能帮我们剩下来不少的时间。（所以时间越多，时间越少？🤔）** 其实这里我想说的是，**既然 AI 有能把代码改好的能力，会不会是我们能力不行？** 我们要学会用正确的姿势使用 AI，你可以不写代码，但你要有让 AI 发现 Bug 的能力，而不是报错了就直接把 Error 甩给 AI 让他修改，当甩手掌柜（至少目前这个时代还没来）。\n2. **思维的禁锢：** 停止思考。Vibe Coding 的方式几乎能让你实现所有你想实现的，**但是程序员的最终归宿真的是你就说能不能跑吗？** 恰恰相反，我觉得在成本可以接受的情况下，代码质量可能更重要。我觉得这可能就是一个留存率的概念，**或许你可以成功做出一款产品让这所有人都为你买一次单，变成第一个吃螃蟹的人，但是如果你的优化不够好，那你的留存率又能有多高呢？** 所以，我觉得 AI Coding 能力的提升，会淘汰掉所有不思考的程序员。在未来，我们可能不写一行代码，但要有优化架构的能力，让 AI 不断按照你的意愿修改，而不是 AI 给我们什么我们就用什么。因此，我感觉在未来普通程序员会 Level Up 一个档次变成架构师，这是对程序员的一次能力考验。\n## 总结\nAI 帮我们省了很多时间，但同时对我们的能力有了新的要求，我们可能需要花更多时间来提高自己。**技术革命一定会淘汰一些人，但希望不要是我** 。\n现在大模型厂商基本几个月就有一轮模型发布，可能过几周就有新模型屠榜，我们就有新 API 可以调用。但随着模型能力不断变强，我们原地踏步，那到底谁才是机器呢？🤨","date":"2026-01-30","path":"/posts/%E4%B8%AA%E4%BA%BA/%E5%BD%93%E7%BC%96%E7%A8%8B%E5%8F%98%E5%BE%97%E5%BB%89%E4%BB%B7%E7%A8%8B%E5%BA%8F%E5%91%98%E5%88%B0%E5%BA%95%E8%BF%98%E5%89%A9%E4%B8%8B%E4%BB%80%E4%B9%88/","permalink":"http://xiaoshidui.top/posts/%E4%B8%AA%E4%BA%BA/%E5%BD%93%E7%BC%96%E7%A8%8B%E5%8F%98%E5%BE%97%E5%BB%89%E4%BB%B7%E7%A8%8B%E5%BA%8F%E5%91%98%E5%88%B0%E5%BA%95%E8%BF%98%E5%89%A9%E4%B8%8B%E4%BB%80%E4%B9%88/","title":"当编程变得廉价，程序员到底还剩下什么"},{"content":"\t上篇已经聊过 1.24 前的 map，这篇让我们看看 1.24 之后的实现 —— 瑞士表（SwissTable）\n# 1 引\n在有一次面试的时候，面试官问了我一个问题，这个问题在我学习 SwissTable 的时候意识到是同一个问题。问题如下：  \n**现在有一百万条数据要做处理，还有一个足够大的内存能放下这一百条数据，现在假设这个数据有两种组织方式，一种是链表，另一种是数组，请问哪一种处理起来效率会更高？**   \n这个问题我当时是这么回答的（当然这里语言组织的比当时好很多^-^）：数组处理效率更高，因为 cpu 把数据从内存读到寄存器的时候，不是一次拿一条数据，而是一次拿一块连续的内存放进 cache。此时如果是链表的情况，链表两个节点在内存空间中地址不连续，就会导致我一次拿的内存可能不会覆盖到下一个节点，那么拿下一个节点的时候，又需要访问，带来了额外开销。而数组不一样，连续的内存让他有了很好的空间局部性，拿一块内存，那下一次访问的数据也大概率会在 cache 中找到，减少了访存的次数，从而提高了性能。\n## 1.1 拉链法的性能瓶颈\n![](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251031215629084.png)\n从上面的这个引子也就知道了，拉链法指针地址的离散型，是对 Cache 不友好的，按照现代计算机的设计来看，离 CPU 越远，获取数据的成本就越高。换言之，我们希望有一种方式能尽可能让 Hash 表中的数据紧密起来，让 CPU 能一次访存，多次复用。  \n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251031215708524.png)\n回到解决 Hash 冲突问题本身，除了拉链法，还有一种就是线性探测法。**线性探测法是在 Hash 碰撞的时候，向后找位置，找到第一个可以放下该元素的槽（slot），接着插入，如果直到链表查完都没有 slot 可用，就触发扩容。查询的时候，会从 Hash 的起始 slot 往后查，直到查到元素或者查到空 slot 或者查完整个表，结束查询。** 线性探测法的链表会呈现出如下的形式。（忘记画slot的状态的，简单看看~）\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251031220016796.png)\n可以看出来如果是线性探测法的 Hash 表，只需要一个数组就能承担责任，而数组是内存连续的地址，妥妥的cache friendly。而且实现起来也相对简单，**每个位置只需要存储 kv 对以及当前 slot 的状态（是空还是已删除还是有数据）**。固然缓存友好，但是线性探测法的缺点在哪里？\n1. 冲突的 Key 总归是要存储起来的，而其存储会占用其他的 Hash 槽，会导致其扩容频率高于拉链法。\n2. 查找过程有大概率会退化到 `O(n)` 的时间复杂度，而且没法进行像 Java 中的 HashMap 那样将链表转化为红黑树的优化。\n## 1.2 单指令流多数据流（SIMD）\n先上结论：**SIMD 在 SwissTable 中的应用很大的提高了 Hash 对比的效率。**\n在具体展开讲 SwissTable 之前，先来回顾一下 SIMD，也就是一条指令操作多个数据。连续内存带来的优势会被 SIMD 进一步放大。与 SIMD 对应的是 SISD，也就是单指令流多数据流。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251031230243882.png)\n单指令流多数据流硬件层面的限制，导致了一条指令只能操作一个数据，无法实现数据并行。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251031230730993.png)\n而在 SIMD 中，每个ALU 前面都有自己独立的局部寄存器组，当 CU 会把指令下发给所有 ALU，然后 ALU 会从自己的局部寄存器组中获取不同的数据，然后进行一样的操作。举个例子：\n`A = [1, 2, 3, 4]`，`B = [4, 3, 2, 1]` 求 A 中元素和 B 中元素对应位置相加。  \n首先在访存的时候，会分别将 A 和 B 的数据按地址分为四块，放入四个 ALU 中的局部寄存器，然后，CU 发 ADD 指令给每个 ALU，ALU 会从读出局部寄存器的值，然后独立的进行操作，从而实现单指令流多数据流。  \n这里就看出了 SIMD 的对数据的基本要求：\n1. 指令操作的数据需要在内存中连续，这样 CPU 在访存的时候，才能一次拿出来，并且分块\n2. 各 ALU 之间操作的数据不能有重叠，且数据的起始地址要对齐到寄存器宽度的整数倍\n3. 所有参与 SIMD 的元素必须是相同类型\n4. 所有元素执行完全相同的运算逻辑\n# 2 SwissTable（折中的取舍）\n在 Go 1.24 之后，底层 map 的实现改为了使用线性探测法的一种数据结构 —— **瑞士表（SwissTable）**。\n1. SwissTable 是在 2017 年的 cppcon 上谷歌工程师正式宣布推出的。Google 的工程师在演讲上表示说在 Google 的集群上，仅 Hash 表这一个数据结构，就占用了约 1 % 的 CPU 和约 4% 的内存，这还只是针对 C++ 的情况，会议视频链接放在文章末尾。\n2. 推动 Go 改用 SwissTable 的 issue 实际上也是由字节团队提出的，他们表示在字节，Go 服务大约消耗了 4% CPU 用于 map。其次，`mapassign()`  和 `mapaccess()` 的比重是 1:1，也就是插入和获取性能同样重要，而 Abseil 和 Rust 早就引入了 SwissTable 作为 map 的实现，所以希望Go 也能改用 SwissTable（毕竟字节是 Go 大户），并给出了 SwissTable 的 Go 的实现。\n上面两个大规模集群的例子中，1% 的 CPU 优化可能带来的是节省上万台服务器，而 1% 的内存优化，带来的可能是数 TB 的 RAM 节省。所以为什么 SwissTable 能有这样的性能？让我们往下看看。\n## 2.1 SwissTable 的结构\n从内存布局上来看，SwissTable 长下面这样：（这个是一般的 SwissTable ，Go 的实现略微有一些差异，但基本上是没区别的）\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251101004730856.png)\n（虽然有点乱，但是有点乱^ _ ^）把上面这一整块看成一块连续的内存空间就行   \nBucket：一个 Bucket 用来存储和标识一个数据，Bucket 由一个 8 位的元数据 + 一个 Slot 组成。其中，Slot 是真正存储 KV 的地方，而 Metadata 是一个 1 字节的信息，其中高位 1 bit 是用来标识当前这个 Slot 的状态的，低 7 bit 是Slot 中存储的 key 经过 hash 计算后的低 7 位。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251101010818096.png)\nGroup：一个 Group 包含了 16 个 metaData 以及 16 个对应的 slot。（**这里16 个 solt 是可以变化的，因为在不同平台，SIMD 支持的并行数据流数据不一样，按照对应平台来**）\n### 2.2.1 SwissTable 的搜索 \n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251103180836540.png)\n1. 首先，key 经过 hash 函数会计算出一个 64 位（不同实现方案会有差异）的 hash 值，hash 值会被分为 h1（高 57 位） 和 h2（低 7 位）。\n2. 接着会使用 h1 对 Group 数取模，进而定位到具体的组（代表从这组还是搜索）\n3. 接着拿出该 Group 对应的控制字节（一般一组是 8 个或 16 个 slot，对应 8个或 16 个控制字节）与 H2 做对比，这里才用并行的方式，提高效率\n4. 对比成功的 Slot 则代表可能与该 Key 一致，需要进一步比对该 Slot 中的 Key 是否与目标 Key一致，一致则返回，结束搜索；否则跳过该 Solt，直到该 Group 搜索完。在搜索过程中，如果遇到一个 Slot 状态为 Empty，则无需继续搜索下一个 Group。\n5. 如果一个 Group 搜完还没找到，则按序搜索下一个 Group，直到找到 Key 或全表搜完。\n**起始比较重要的操作就是 SwissTable 的查找和插入，具体的插入操作因为涉及到了 map 的扩容，所以在下面第三章结合 Go 的具体实现展开聊**\n\n---\n\n到这里，我们可以观察一下，SwissTable 先是利用了一块完整的内存空间，进而提高了 Cache （热数据）的命中率；另一方面，利用 SIMD 一次性对比多个 Slot 相较于传统的线性探测法的实现，又提升的查找的速度。那么代价是什么呢？\n1. 对于内存的空间利用率远不如拉链法，拉链法能很好的利用内存中的碎片。\n2. 因为其要求一整张表在一片连续的内存空间中，在扩容的时候，需要 Get 一片更大的连续的空间，并且需要把原来的全部 kv 对 rehash 到新的表中。\n3. 实现的难度相较于拉链法更难，因为除了键值存储，还需要存储槽位的状态。\n其实两种方法都各有好坏，需要按照具体场景具体分析。\n# 3 Go 的 SwissTable 实现\n## 3.1 Map 的结构\n我们先来看关于 Go 的 map（就是 SwissTable，下面用 map 代替，不然太长了^ _ ^） 的三个重要承载 Map 数据结构。我们从内到外，有细到粗将 Map 分为三个结构，分别是 Group、Table 和 Map。一个 Map 中包含一个或多个 Table，一个 Table 包含多个 Group。\n### 3.1.1 Group\n首先是 `groupReference` 和 `groupsReference`，`groupReference` 指的是一个组，不同于 SwissTable 的一般实现，go 的实现是槽后面紧跟 slot；而 `groupsReference` 就是多个 `groupReference`，但是内存是一次性连续分配的，也就是说 `groupsReference` 开辟了一块完整的内存空间，里面有多个 `groupReference`。他们是 map 底层存储数据的地方。一个组内最多 8 个 Slot（**后续会解释为什么只有 8 个 Slot，而不是16**）。  \n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251104004010976.png)\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251104004148506.png)\n\n```go\n// 多个组\ntype groupsReference struct {\n\t// 指向一个 group 数组的起始地址\n\tdata unsafe.Pointer // data *[length]typ.Group\n\t// lengthMask = length（group的数量） - 1\n\t// 要求 length 一定是 2 的幂，可以用 lengthMask 位运算快速取模\n\t// 在计算某个 key 应该落到那个 group 的时候\n\t// 利用 h1 \u0026 lengthMask 代替 h1 % length，加速操作\n\tlengthMask uint64\n}\n\n// 单个组 —— group\ntype groupReference struct {\n\t// data points to the group, which is described by typ.Group and has\n\t// layout:\n\t//\n\t// type group struct {\n\t// \tctrls ctrlGroup\n\t// \tslots [abi.MapGroupSlots]slot\n\t// }\n\t//\n\t// type slot struct {\n\t// \tkey  typ.Key\n\t// \telem typ.Elem\n\t// }\n\tdata unsafe.Pointer // data *typ.Group\n}\n```\n 在 `groupReference` 的注释中可以看出，单个组内的内存分布情况。这两个结构体都是有一个指针指向一块连续的内存空间，其中，`groupReference` 指向一个组，而 `groupsReference` 指向一个 [length]groupReference。  \n lengthMask：lengthMask = groupLength - 1。当我们求一个 key 落到哪个 group 的时候，是用 `h1 % groupLength` 以获取的，而这个取模操作实际上可以用位运算来代替。即 `h1 % groupLength = h1 \u0026 (groupLength - 1)` ，而位运算是比除法快的。\n### 3.1.2 Table\nTable 是对 groupsReference 的上层封装，因为 groupsReference 实际上只负责存储数据，少了很多关键字段，而 Table 是对 groupsReference 的进一步封装，一个 Table 底层对应一个 groupReference，对应多个组，所以我们在理解 Table 的时候，把一个 Table 理解为一个 SwissTable 即可。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251104144416514.png)\n一个 Table 中最多能有 1024 个 Slot，转为 Group 即一个 Table 中最多 128 个 Group（一个 Group 中固定 8 个 Slot）。\n ```go\n type table struct {\n\t// 表中元素的个数\n\tused uint16\n\t// 表的槽的数量，因为一个表中包含多个 group，\n\t// 所以表的总槽数 = group 数 * 一个 group 中的 slot 数\n\tcapacity uint16\n\t// 表在下次触发 rehash 之前还能插入多少数据\n\t// rehash 是指触发扩容或者 rehash\n\tgrowthLeft uint16\n\t// 这一位实际上是当前 table 使用的目录位深度\n\t// 当我们在扩容 table 分裂的时候，用来定位 table 的位数也在上涨\n\t// 这时 2 ^ (globalDepth - localDepth) \n\t// 用来标识当前 table 被几个页目录共享（详细看后文）\n\tlocalDepth uint8\n\t// 该 table 在 map 目录中的索引的起始位置（因为一个表可能被多个连续目录项指向）\n\tindex int\n\t// 一个 group 数组的引用，真正存储数据的地方\n\tgroups groupsReference\n}\n```\nused：标识当前 table 中有多少 kv 对。\ncapacity：标识当前 table 中的总槽数（group 数 * 8）。\ngrowthLeft：在下次触发 rehash（扩容） 之前，还能插入多少 kv 对。\nlocalDepth：标识的是该 table 所处的深度。该字段要结合 Map 中的 globalDepth 来理解。看后文中的扩容。\nindex：因为在 Map 中可能会有多个目录项都指向同一个 table，所以 index 记录 Map 的表目录中，第一个所处的索引，例如表目录中，4、5 都指向该 table，那么这个 table 的 index 就是 4。\ngroups：指向真正存储数据的 group 数组。\n### 3.1.3 Map\nMap 结构体，我们每次创建一个 Map 的时候，实际上就是创建了一个 Map 结构体\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251104162022290.png)\n```Go\ntype Map struct {\n\t// map中当前有多少 slot 被占了，数值等于 map 中的 kv 对\n\tused uint64\n\t// 随机 hash 种子\n\tseed uintptr\n\t// 表目录，这个目录指向 map 中所有的 table\n\t// 一个 table 内地址连续，一个 table 内包含多个 group\n\t// 当小表优化的时候，该字段指向一个 group\n\tdirPtr unsafe.Pointer\n\t// 目录长度\n\tdirLen int\n\t// 使用 hash 的多少位来定位 table\n\tglobalDepth uint8\n\t// 需要右移多少位来定位 table，这个值在 64 位平台是 64 - globalDepth\n\tglobalShift uint8\n\t// 是否有协程在写\n\twriting uint8\n\t// 墓碑位标记（Clear 的时候会用，暂时可以不用看）\n\ttombstonePossible bool\n\tclearSeq uint64\n}\n```\nused：标记当前 map 中有多少 kv 对，反过来就是有多少 slot 被占用了\nseed：随机 hash 种子，创建表的时候会 rand 生成一次，在 map 中 kv 被清空的时候，又会重新生成一次，主要是处于安全性的考量\ndirPtr：指向真实存储数据的地方，其指向有两种可能。如果是当前 map 中预估的数据量很小，不超过 8 个 kv，则会创建小 map，也就是直接将 dirPtr 指向一个 Group；如果当前数据量增大了或者一开始创建请求的量就比较大，此时 dirPtr 会指向一个 Table 数组，里面存储一个或多个 Table。\ndirLen：table 目录的长度。\nglobalDepth：全局表的深度，其表示需要 h1 的高多少位来选择 table 数组中的 table。随着 table 数量的增加，需要的位数也会越多。\nwriting：写标记位，依旧不支持并发安全哦。\n## 3.2 Go SwissTable 实现\n下面分别从 Map 的创建、搜索、插入、删除、扩容以及遍历的 Go 的实现逐一展开，并且结合相关源码。\n### 3.2.1 创建\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105144246154.png)\n```go\nfunc NewMap(mt *abi.MapType, hint uintptr, m *Map, maxAlloc uintptr) *Map {\n\t// 未指定 map，则创建一个空 Map 对象\n\tif m == nil {\n\t\tm = new(Map)\n\t}\n\t\n\t// 设置随机 Hash 种子\n\tm.seed = uintptr(rand())\n\t\n\t// 当预估插入数量小于组内的槽数时，小表优化\n\tif hint \u003c= abi.MapGroupSlots {\n\t\t// 这里懒分配，插入的时候再进行分配\n\t\treturn m\n\t}\n\t\n\t// 计算按装载因子来装载，装满hint个元素需要多少 slot (MapGroupSlots = 8)\n\t// 换言之分配多少 slot 不会超过装载因子\n\ttargetCapacity := (hint * abi.MapGroupSlots) / maxAvgGroupLoad\n\tif targetCapacity \u003c hint { // overflow\n\t\treturn m // return an empty map.\n\t}\n\n\t// maxTableCapcity = 1024 单张表存储的 slot 上限\n\t// 首先把目标槽数按每张表的最大容量来分，计算出需要多少张表\n\t// 然后将表的数量对齐到 2 ^ N，对齐到 2 ^ N有诸多好处，方便位操作\n\t// 而且在扩容的时候也有好处\n\tdirSize := (uint64(targetCapacity) + maxTableCapacity - 1) / maxTableCapacity\n\tdirSize, overflow := alignUpPow2(dirSize)\n\tif overflow || dirSize \u003e uint64(math.MaxUintptr) {\n\t\treturn m // return an empty map.\n\t}\n\t\n\t// hint 明显过大，懒分配，返回空 map\n\tgroups, overflow := math.MulUintptr(uintptr(dirSize), maxTableCapacity)\n\tif overflow {\n\t\treturn m // return an empty map.\n\t} else {\n\t\tmem, overflow := math.MulUintptr(groups, mt.GroupSize)\n\t\tif overflow || mem \u003e maxAlloc {\n\t\t\treturn m // return an empty map.\n\t\t}\n\t}\n\t\n\t// log2(dirSize) globalDepth 需要几位来定位 Table\n\tm.globalDepth = uint8(sys.TrailingZeros64(dirSize))\n\t// 平台位数 - globalDepth 计算出取 globalDepth 的时候需要右移多少位\n\tm.globalShift = depthToShift(m.globalDepth)\n\t\n\t// 给 dir 开辟空间，能走到这里证明不需要小表优化\n\t// 所以 dir 指向的一定是 table \n\tdirectory := make([]*table, dirSize)\n\t\n\t// 按照目录项数，分别分配 table\n\tfor i := range directory {\n\t\tdirectory[i] = newTable(mt, uint64(targetCapacity)/dirSize, i, m.globalDepth)\n\t}\n\t\n\tm.dirPtr = unsafe.Pointer(\u0026directory[0])\n\tm.dirLen = len(directory)\n\t\n\treturn m\n}\n```\n（1）如果调用方没有预创建 Map 传入，则创建一个新的 Map，并设置随机 Hash 种子\n``` go\n// 未指定 map，则创建一个空 Map 对象\nif m == nil {\n\tm = new(Map)\n}\n\n// 设置随机 Hash 种子\nm.seed = uintptr(rand())\n```\n（2）当预估的容量很小的时候（小 map 模式，也就是预计插入容量小于 8 个 slot），会采用懒分配，等到插入的时候再分配具体的存储区域。\n```go\n// 当预估插入数量小于组内的槽数时，小表优化\nif hint \u003c= abi.MapGroupSlots {\n\t// 这里懒分配，插入的时候再进行分配\n\treturn m\n}\n```\n（3）会按照负载因子计算，分配多少个 slot 不会超过负载因子，因为一组最大只能存 8 个 slot。如果计算完发现 targetCapacity \u003c hint 预估容量，则代表溢出了，暂时不分配存储空间，等插入的时候动态分配。\n```go\ntargetCapacity := (hint * abi.MapGroupSlots) / maxAvgGroupLoad\nif targetCapacity \u003c hint { // overflow\n\treturn m // return an empty map.\n}\n```\n（4）因为单个 table 上限是只有 1024 个 slot，128 个 group，所以要根据预估的 slot 数计算需要多少个 table。这里要求 table 的数量一定是 2^N，向 2 对齐有诸多好处。比如说预估需要 3 个 table，则会将 table 数对齐到 4。如果溢出了，则也是采用懒分配。\n```go\ndirSize := (uint64(targetCapacity) + maxTableCapacity - 1) / maxTableCapacity\ndirSize, overflow := alignUpPow2(dirSize)\nif overflow || dirSize \u003e uint64(math.MaxUintptr) {\n\treturn m // return an empty map.\n}\n```\n（5）接着是计算有没有能力一次性分配这么多空间，如果没有，则懒分配\n ```go\n groups, overflow := math.MulUintptr(uintptr(dirSize), maxTableCapacity)\nif overflow {\n\treturn m // return an empty map.\n} else {\n\tmem, overflow := math.MulUintptr(groups, mt.GroupSize)\n\tif overflow || mem \u003e maxAlloc {\n\t\treturn m // return an empty map.\n\t}\n}\n ```\n （6）接着就是分配对应的空间，与初始化 Map 中的字段。先初始化全局目录深度字段。接着会给表目录分配空间（代码能执行到这里，一定不是小 map，小 map 会在前面懒分配直接返回）。接着就是给每个表分配内存空间。\n ```go\n // log2(dirSize) globalDepth 需要几位来定位 Table\nm.globalDepth = uint8(sys.TrailingZeros64(dirSize))\n// 平台位数 - globalDepth 计算出取 globalDepth 的时候需要右移多少位\nm.globalShift = depthToShift(m.globalDepth)\n\n// 给 dir 开辟空间，能走到这里证明不需要小表优化\n// 所以 dir 指向的一定是 table \ndirectory := make([]*table, dirSize)\n\n// 按照目录项数，分别分配 table\nfor i := range directory {\n\tdirectory[i] = newTable(mt, uint64(targetCapacity)/dirSize, i, m.globalDepth)\n}\nm.dirPtr = unsafe.Pointer(\u0026directory[0])\nm.dirLen = len(directory)\n\nreturn m\n ```\n 下面就是创建单个 table 的逻辑，代码也很简单，主要是做一些校验，然后会调用 `reset()`，真正给表内分配 groupsReference，也就是真实存储数据的连续内存。这里的代码比较简单，看我的注释即可。\n ```go\n func newTable(typ *abi.MapType, capacity uint64, index int, localDepth uint8) *table {\n\t// 检查单表 solt 数，如果少于单组最小 slot 数，则对齐到单表最小 slot 数\n\tif capacity \u003c abi.MapGroupSlots {\n\t\tcapacity = abi.MapGroupSlots\n\t}\n\t\n\tt := \u0026table{\n\t\tindex:      index,\n\t\tlocalDepth: localDepth,\n\t}\n\t\n\t// 检查 slot 数有没有超过单表最大 slot 数\n\tif capacity \u003e maxTableCapacity {\n\t\tpanic(\"initial table capacity too large\")\n\t}\n\t\n\t// slot 数对齐到 2 的幂\n\t// 这里对齐后 slot 数一定是 8 的倍数，也就是一定可以分成一定数量的 group\n\tcapacity, overflow := alignUpPow2(capacity)\n\tif overflow {\n\t\tpanic(\"rounded-up capacity overflows uint64\")\n\t}\n\t\n\t// 真实分配真正的存储部分\n\t// 按照 capacity 计算出组数，接着按照组数分配出一片内存空间作为该 table\n\t// 按照 group 和 map 元数据计算出每组所在的位置，并且将对应组的控制数组置为 empty\n\tt.reset(typ, uint16(capacity))\n\n\treturn t\n}\n\n// 为 table 创建真正存储数据的地方，并且初始化每个组的控制字节\nfunc (t *table) reset(typ *abi.MapType, capacity uint16) {\n\t// 按照总 slot 数，计算组数\n\tgroupCount := uint64(capacity) / abi.MapGroupSlots\n\t// 分配底层的数组（注意，一个 table 内的组占用的是一块连续的地址）\n\tt.groups = newGroups(typ, groupCount)\n\tt.capacity = capacity\n\t// 计算 rehash 前，还能插入多少元素\n\tt.growthLeft = t.maxGrowthLeft()\n\n\t// 初始化每个 group 的 控制字节位 empty\n\tfor i := uint64(0); i \u003c= t.groups.lengthMask; i++ {\n\t\tg := t.groups.group(typ, i)\n\t\tg.ctrls().setEmpty()\n\t}\n}\n\n// 按照组数分配一片内存用来存储每个组\nfunc newGroups(typ *abi.MapType, length uint64) groupsReference {\n\treturn groupsReference{\n\t\tdata:       newarray(typ.Group, int(length)),\n\t\tlengthMask: length - 1,\n\t}\n}\n ```\n### 3.2.2 搜索\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105153734533.png)\n通过指定 Key 获取 map 中元素的依旧对应两个方法，分别是 `runtime_mapaccess1()` 和  `runtime_mapaccess2()` 这里围绕 1 展开，关注具体的搜索细节。\n```go\nfunc runtime_mapaccess1(typ *abi.MapType, m *Map, key unsafe.Pointer) unsafe.Pointer {\n\t// 竞态检测或内存检测\n\t// ...\n\n\t// 如果 m 是 nil 或者空，直接返回零值\n\tif m == nil || m.Used() == 0 {\n\t\tif err := mapKeyError(typ, key); err != nil {\n\t\t\tpanic(err) // see issue 23734\n\t\t}\n\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t}\n\n\t// 并发读写报错\n\tif m.writing != 0 {\n\t\tfatal(\"concurrent map read and map write\")\n\t}\n\n\t// 根据 key 计算出 hash 值\n\thash := typ.Hasher(key, m.seed)\n\n\t// 如果 dirlen \u003c= 0 的时候是小 map\n\tif m.dirLen \u003c= 0 {\n\t\t// 走小 map 的查询流程\n\t\t_, elem, ok := m.getWithKeySmall(typ, hash, key)\n\t\tif !ok {\n\t\t\t// 小 map 中查询没有，则直接返回零值\n\t\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t\t}\n\t\treturn elem\n\t}\n\n\t// Select table.\n\t// 根据 hash 值定位 table_idx，这里实际上就是用 hash 的高位去判断属于哪个 table\n\tidx := m.directoryIndex(hash)\n\t// 获取到具体的 table （table 包含了多个 group）\n\tt := m.directoryAt(idx)\n\n\t// Probe table.\n\t// 按照 h1 \u0026 lengthmask，获取一个探测序列\n\t// 也就是说一样的 key，探测序列一定一样，从同一个起始点出发\n\tseq := makeProbeSeq(h1(hash), t.groups.lengthMask)\n\t// hash 值的低 7 位\n\th2Hash := h2(hash)\n\tfor ; ; seq = seq.next() {\n\t\t// 根据探测序列获取到具体的组\n\t\tg := t.groups.group(typ, seq.offset)\n\t\t// 返回该组中和该 h2 匹配的 slot\n\t\tmatch := g.ctrls().matchH2(h2Hash)\n\t\t\n\t\t// match != 0 ==\u003e match 中只要有一位是 1 ==\u003e 这组中至少有一个 key 匹配上了\n\t\tfor match != 0 {\n\t\t\ti := match.first()\n\t\t\t// 对比\n\t\t\tslotKey := g.key(typ, i)\n\t\t\tslotKeyOrig := slotKey\n\t\t\tif typ.IndirectKey() {\n\t\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t\t}\n\t\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\t\tslotElem := unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\t\tif typ.IndirectElem() {\n\t\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t\t}\n\t\t\t\treturn slotElem\n\t\t\t}\n\t\t\t// 移除匹配到的槽，循环继续查找\n\t\t\tmatch = match.removeFirst()\n\t\t}\n\t\t\n\t\t// 到这里有两种可能：\n\t\t// 1. 组内匹配完毕，发现没有匹配的 KV\n\t\t// 2. match = 0，即这一组内没有 K 与目标 Key 匹配\n\t\t// 检测该组有没有 empty 槽\n\t\tmatch = g.ctrls().matchEmpty()\n\t\t// 如果有 empty 槽，则直接返回对应类型的 0 值\n\t\tif match != 0 {\n\t\t\t// Finding an empty slot means we've reached the end of\n\t\t\t// the probe sequence.\n\t\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t\t}\n\t\t// seq.next() 中只有取模，没有边界 check\n\t\t// 负载因子的存在保证了 table 一定有空 slot\n\t\t// 所以只有两种可能性，一种是存在 key 并且找到\n\t\t// 另外一种是 key 不存在，找到了空槽，结束查找\n\t}\n}\n```\n（1）如果 map 为空或者为 nil，则直接返回对应 val 类型的零值。\n```go\nif m == nil || m.Used() == 0 {\n\tif err := mapKeyError(typ, key); err != nil {\n\t\tpanic(err) // see issue 23734\n\t}\n\treturn unsafe.Pointer(\u0026zeroVal[0])\n}\n```\n（2）查看是否有协程正在并发写入 map，如果有则直接 fatel，否则计算出查询 key 的 hash 值\n```go\n// 并发读写报错\nif m.writing != 0 {\n\tfatal(\"concurrent map read and map write\")\n}\nhash := typ.Hasher(key, m.seed)\n```\n（3）查看是否是小 map，如果是小 map，则走小 map 的查询逻辑（因为如果是小 map，则 Map 中的 dirPtr 字段直接指向的是一个 group，而不是 table 数组）\n```go\n// 如果 dirlen \u003c= 0 的时候是小 map\nif m.dirLen \u003c= 0 {\n\t// 走小 map 的查询流程\n\t_, elem, ok := m.getWithKeySmall(typ, hash, key)\n\tif !ok {\n\t\t// 小 map 中查询没有，则直接返回零值\n\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t}\n\treturn elem\n}\n```\n（4）通过 hash 值的高几位去定位该 k 所处的 Table，具体取高多少位随着 map 扩容会动态改变，主要参考 globalShift 和 globalDepth 字段。定位到具体的 Table 后，会根据 Key 的 h1 定位  table 中所处的 group，进而生成一个探测序列。（**探测序列就是指从那个 group 开始搜索，因为线性探测法会导致 kv 落在其他 group 的 slot 中。同一个 key 的探测序列是一致的**）\n```go\nfunc (m *Map) directoryIndex(hash uintptr) uintptr {\n\tif m.dirLen == 1 {\n\t\treturn 0\n\t}\n\t// (m.globalShift \u0026 63) 计算定位 table 需要右移的位数\n\t// hash 右移 x 位，进而定位到具体的 table\n\treturn hash \u003e\u003e (m.globalShift \u0026 63)\n}\n\n// 根据 hash 值定位 table_idx，这里实际上就是用 hash 的高位去判断属于哪个 table\nidx := m.directoryIndex(hash)\n// 获取到具体的 table （table 包含了多个 group）\nt := m.directoryAt(idx)\n// Probe table.\n// 按照 h1 \u0026 lengthmask，获取一个探测序列\n// 也就是说一样的 key，探测序列一定一样，从同一个起始点出发\nseq := makeProbeSeq(h1(hash), t.groups.lengthMask)\n// hash 值的低 7 位\nh2Hash := h2(hash)\n```\n（5）按照探测序列一个一个组的探测，组内的探测流程可以见下一小节中的小 map 探测。当一个组匹配完，如果没有找到目标 k，则会探测下一个组，直到探测到空的 Slot 或者目标 key。这里实际上是有两个我们要知道的前提：\n1. **因为有负载系数的存在，所以 table 一定不会存满，也就是说 table 中一定有状态为 empty 的 Slot。**\n2. **如果当前组内有一个 empty 的 slot，那么目标 key 要么存在于当前组，要么不存在，不可能存在于要探测的下一个组。（因为在删除元素的时候，会将位置置为 deleted 而不是 empty）**\n基于上面两个大的前提，代码也就很明了了，会在判断完当前探测的组内是否有 key 后，如果没有，则会进一步判断当前组内是否有 empty 槽，如果有，则会停止探测下一个组。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105161629584.png)\n```go\nfor ; ; seq = seq.next() {\n\t// 根据探测序列获取到具体的组\n\tg := t.groups.group(typ, seq.offset)\n\t// 返回该组中和该 h2 匹配的 slot\n\tmatch := g.ctrls().matchH2(h2Hash)\n\t\n\tfor match != 0 {\n\t\ti := match.first()\n\t\t// 对比\n\t\tslotKey := g.key(typ, i)\n\t\tslotKeyOrig := slotKey\n\t\tif typ.IndirectKey() {\n\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t}\n\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\tslotElem := unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t}\n\t\t\treturn slotElem\n\t\t}\n\t\t// 移除匹配到的槽，循环继续查找\n\t\tmatch = match.removeFirst()\n\t}\n\t\n\tmatch = g.ctrls().matchEmpty()\n\tif match != 0 {\n\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t}\n}\n```\n#### 小 map 搜索流程（单个组的搜索流程）\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105154819027.png)\n\n```go\nfunc (m *Map) getWithKeySmall(typ *abi.MapType, hash uintptr, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer, bool) {\n\t// 当小 map 的时候，dir 指针指向的不是 table 数组，而是一个单个的 group\n\t// 所以这里能直接获取到这个 group\n\tg := groupReference{\n\t\tdata: m.dirPtr,\n\t}\n\n\t// h2(hash) 获取低 7 位\n\t// 这里得到一个 64 位bitset，其中每个字节的最高位如果为 1 代表该字节对应的 slot 匹配成功\n\tmatch := g.ctrls().matchH2(h2(hash))\n\n\tfor match != 0 {\n\t\t// 获取第一个匹配到的槽的 idx\n\t\ti := match.first()\n\t\t// 根据索引 取到 key\n\t\tslotKey := g.key(typ, i)\n\t\tif typ.IndirectKey() {\n\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t}\n\t\t// 判断 key 是否相等\n\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\tslotElem := g.elem(typ, i)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t}\n\t\t\t// 相等直接返回\n\t\t\treturn slotKey, slotElem, true\n\t\t}\n\n\t\t// 将右边第一个 1 置为 0，往左寻找下一个 1（匹配到的槽）\n\t\tmatch = match.removeFirst()\n\t}\n\n\t// No match here means key is not in the map.\n\t// (A single group means no need to probe or check for empty).\n\t// 在 map 中没有找到该 key，直接返回\n\treturn nil, nil, false\n}\n```\n（1）直接获取到 group 数组\n```go\ng := groupReference{\n\tdata: m.dirPtr,\n}\n```\n（2）接着，通过 matchH2 方法，并行的比较 H2 与 group 中的 8 个slot，然后返回一个 8 字节的 bitset，如果某一个字节是 1，则代表这一字节对应的 slot 与该 key 可能一致，需要进一步比对 key。\n```go\nmatch := g.ctrls().matchH2(h2(hash))\n\nconst bitsetLSB   = 0x0101010101010101 // 每字节最低位 = 1\nconst bitsetMSB   = 0x8080808080808080 // 每字节最高位 = 1\n\n// 返回 H2 匹配到的 slot\nfunc (g ctrlGroup) matchH2(h uintptr) bitset {\n\treturn ctrlGroupMatchH2(g, h)\n}\n// 位运算找到 h2 值匹配的槽\nfunc ctrlGroupMatchH2(g ctrlGroup, h uintptr) bitset {\n\t// bitsetLSB = 0x0101010101010101\n\t// (bitsetLSB * uint64(h)) ==\u003e 乘法广播\n\t// 假设 h2 是 0x42 * bitsetLSB ==\u003e 0x4242424242424242\n\t// 为了和八个槽并行比对，所以这里要把 h 广播到 64 位\n\t// 接着和 g 的 8 个 ctrl 位异或，这里要注意 g 的高位为 0 的时候，才表示这个槽内有数据\n\t// 而 h2 是低 7 位，最高位也是 0，即当 g 与 h 完全匹配的时候，高位异或应该也是 0\n\t// 也就是说，如果某个 ctrl 匹配到了，这个位置 8 个 bit 应该全为 0\n\t// 也就是说，这里的 v 中，如果全为 0 的字节对应的槽，即为匹配上的槽\n\tv := uint64(g) ^ (bitsetLSB * uint64(h))\n\t// bitsetMSB = 0x8080808080808080\n\t// 这一步是检测零字节，返回一个 bitset\n\t// 这个 bitset 的每个字节的最高位如果是 1，则代表匹配\n\t// 比如说 10000000 ==\u003e 则这这一字节对应的 slot 匹配成功\n\treturn bitset(((v - bitsetLSB) \u0026^ v) \u0026 bitsetMSB)\n}\n```\n（3）同普通 map 的组内校验。\n```go\nfor match != 0 {\n\t// 获取第一个匹配到的槽的 idx\n\ti := match.first()\n\t// 根据索引 取到 key\n\tslotKey := g.key(typ, i)\n\tif typ.IndirectKey() {\n\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t}\n\t// 判断 key 是否相等\n\tif typ.Key.Equal(key, slotKey) {\n\t\tslotElem := g.elem(typ, i)\n\t\tif typ.IndirectElem() {\n\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t}\n\t\t// 相等直接返回\n\t\treturn slotKey, slotElem, true\n\t}\n\t// 将右边第一个 1 置为 0，往左寻找下一个 1（匹配到的槽）\n\tmatch = match.removeFirst()\n}\n```\n（4）没找到则 `return nil, nil, false\n### 3.2.3 插入\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105171516094.png)\n插入指定的 kv 对，如果 kv 对存在，则修改 k 对应的 v 值。\n```go\n//go:linkname runtime_mapassign runtime.mapassign\n// 插入值，这里依旧是找到插入的位置后，返回对应的地址，由上层调用方插入\nfunc runtime_mapassign(typ *abi.MapType, m *Map, key unsafe.Pointer) unsafe.Pointer {\n\t// 工具配置...\n\t// ...\n\t\n\tif m.writing != 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\t\n\t// 求出 key 的 hash 值\n\thash := typ.Hasher(key, m.seed)\n\t\n\t// 设置写 flag\n\tm.writing ^= 1\n\t\n\t// 如果 dirPtre = nil，代表是小map，小表优化懒加载\n\tif m.dirPtr == nil {\n\t\t// 创建小表并且初始化\n\t\tm.growToSmall(typ)\n\t}\n\t\n\t// dirlen == 0 代表是小表\n\tif m.dirLen == 0 {\n\t\tif m.used \u003c abi.MapGroupSlots {\n\t\t\t// 组内还有空余 slot\n\t\t\telem := m.putSlotSmall(typ, hash, key)\n\t\t\t\n\t\t\tif m.writing == 0 {\n\t\t\t\tfatal(\"concurrent map writes\")\n\t\t\t}\n\t\t\t// 恢复写 flag\n\t\t\tm.writing ^= 1\n\t\t\t\n\t\t\treturn elem\n\t\t}\n\t\t\n\t\t// 没有找到空位，则从小 map （group）扩容成 table，后往下执行 table 的插入流程\n\t\tm.growToTable(typ)\n\t}\n\t\n\t// 找到第一个可以插入的位置\n\tvar slotElem unsafe.Pointer\nouter:\n\tfor {\n\t\t// Select table.\n\t\t// 根据 hash 定位到 table\n\t\tidx := m.directoryIndex(hash)\n\t\tt := m.directoryAt(idx)\n\t\t\n\t\t// 创建探测序列\n\t\tseq := makeProbeSeq(h1(hash), t.groups.lengthMask)\n\t\t\n\t\t// 第一个有删除状态的 group 和对应的 slot，可以用这个 slot 插入新的元素\n\t\tvar firstDeletedGroup groupReference\n\t\tvar firstDeletedSlot uintptr\n\t\t\n\t\th2Hash := h2(hash)\n\t\t// 按照探测序列探测\n\t\tfor ; ; seq = seq.next() {\n\t\t\tg := t.groups.group(typ, seq.offset)\n\t\t\t// 找到 group 内 hash 一致的 slot\n\t\t\tmatch := g.ctrls().matchH2(h2Hash)\n\t\t\t\n\t\t\t// Look for an existing slot containing this key.\n\t\t\t// 遍历 bitset，看是否存在一样的 key\n\t\t\tfor match != 0 {\n\t\t\t\ti := match.first()\n\t\t\t\t\n\t\t\t\tslotKey := g.key(typ, i)\n\t\t\t\tslotKeyOrig := slotKey\n\t\t\t\tif typ.IndirectKey() {\n\t\t\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t\t\t}\n\t\t\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\t\t\t// 匹配成功，更新 key\n\t\t\t\t\tif typ.NeedKeyUpdate() {\n\t\t\t\t\t\ttypedmemmove(typ.Key, slotKey, key)\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tslotElem = unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\t\t\tif typ.IndirectElem() {\n\t\t\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tt.checkInvariants(typ, m)\n\t\t\t\t\t// 退出\n\t\t\t\t\tbreak outer\n\t\t\t\t}\n\t\t\t\t// 找下一个 group\n\t\t\t\tmatch = match.removeFirst()\n\t\t\t}\n\t\t\t\n\t\t\t// 代码走到这里代表没有 table 中没有一致的 key，但是此时探测序列可能还没有结束\n\t\t\t// 获取 group 中的 empty slot\n\t\t\tmatch = g.ctrls().matchEmpty()\n\t\t\t// 注意这里是 if match 不是 for match\n\t\t\tif match != 0 {\n\t\t\t\t// 找到空槽意味着到达了探测序列的终点\n\t\t\t\tvar i uintptr\n\t\t\t\t\n\t\t\t\t// 如果之前有墓碑位，则使用墓碑位插入\n\t\t\t\tif firstDeletedGroup.data != nil {\n\t\t\t\t\tg = firstDeletedGroup\n\t\t\t\t\ti = firstDeletedSlot\n\t\t\t\t\t// 等于暂时将墓碑位所占据的位置释放\n\t\t\t\t\t// 逻辑上就是 table 多了一个能插入的位置\n\t\t\t\t\tt.growthLeft++ \n\t\t\t\t} else {\n\t\t\t\t\t// Otherwise, use the empty slot.\n\t\t\t\t\t// 如果没有墓碑槽，这里直接用空槽\n\t\t\t\t\ti = match.first()\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If there is room left to grow, just insert the new entry.\n\t\t\t\t// 如果在 rehash 之前还能插入，则直接插入\n\t\t\t\tif t.growthLeft \u003e 0 {\n\t\t\t\t\tslotKey := g.key(typ, i)\n\t\t\t\t\tslotKeyOrig := slotKey\n\t\t\t\t\tif typ.IndirectKey() {\n\t\t\t\t\t\tkmem := newobject(typ.Key)\n\t\t\t\t\t\t*(*unsafe.Pointer)(slotKey) = kmem\n\t\t\t\t\t\tslotKey = kmem\n\t\t\t\t\t}\n\t\t\t\t\ttypedmemmove(typ.Key, slotKey, key)\n\t\t\t\t\t\n\t\t\t\t\tslotElem = unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\t\t\tif typ.IndirectElem() {\n\t\t\t\t\t\temem := newobject(typ.Elem)\n\t\t\t\t\t\t*(*unsafe.Pointer)(slotElem) = emem\n\t\t\t\t\t\tslotElem = emem\n\t\t\t\t\t}\n\t\t\t\t\t// 设置 slot 对应的控制字节，可插入位置 --\n\t\t\t\t\tg.ctrls().set(i, ctrl(h2Hash))\n\t\t\t\t\tt.growthLeft--\n\t\t\t\t\tt.used++\n\t\t\t\t\tm.used++\n\t\t\t\t\t// 退出\n\t\t\t\t\tt.checkInvariants(typ, m)\n\t\t\t\t\tbreak outer\n\t\t\t\t}\n\t\t\t\t// 走到这里代表此时虽然表内有空槽\n\t\t\t\t// 但是整个表已经达到负载边界，需要扩容 rehash\n\t\t\t\tt.rehash(typ, m)\n\t\t\t\t// 因为 rehash 了，所以直接重新走一遍插入流程\n\t\t\t\tcontinue outer\n\t\t\t}\n\t\t\t// 这里尝试找有没有墓碑（delete 槽）\n\t\t\t// 如果找到，则将 delete 槽赋值给变量，后面会将元素插入该位置\n\t\t\tif firstDeletedGroup.data == nil {\n\t\t\t\t// Since we already checked for empty slots\n\t\t\t\t// above, matches here must be deleted slots.\n\t\t\t\tmatch = g.ctrls().matchEmptyOrDeleted()\n\t\t\t\tif match != 0 {\n\t\t\t\t\tfirstDeletedGroup = g\n\t\t\t\t\tfirstDeletedSlot = match.first()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 检查写状态有没有被破坏\n\tif m.writing == 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\tm.writing ^= 1\n\n\t// 返回插入位置\n\treturn slotElem\n}\n```\n（1）检测是否有其他协程在并发写 map，如果没有则计算出 key 的 hash 值，并且设置写标记，否则，抛出错误。\n```go\nif m.writing != 0 {\n\tfatal(\"concurrent map writes\")\n}\n\t\n// 求出 key 的 hash 值\nhash := typ.Hasher(key, m.seed)\n\t\n// 设置写 flag\nm.writing ^= 1\n```\n（2） 如果是小 map 的话，在创建的时候会采用懒加载的方式分配空间，所以在这里插入元素的时候，需要分配对应的空间。在 `growToSmall` 中，将开辟一个单位大小的 group 给 Map，Map 的表目录指针也会指向这组。\n```go\nfunc (m *Map) growToSmall(typ *abi.MapType) {\n\t// 创建一个新的 group，长度为 1\n\tgrp := newGroups(typ, 1)\n\tm.dirPtr = grp.data\n\n\tg := groupReference{\n\t\tdata: m.dirPtr,\n\t}\n\t// 组内控制置空\n\tg.ctrls().setEmpty()\n}\n\nif m.dirPtr == nil {\n\t// 创建小表并且初始化\n\tm.growToSmall(typ)\n}\n```\n（3）如果 `dirlen \u003c= 0`，代表此时是小 map，则进入小 map 的插入流程。首先会 check 这个组内是否有空的槽，如果有，则会调用 `putSoltSmall()` 插入。\n在 `putSoltSmall()` 中\n1. 首先会先通过hash 值的 h2，在组内找到所有 h2 一致的 slot，与前面的查找过程一致。如果找到了，则会直接进行赋值，然后返回。如果没找到，则会进入插入流程。\n2. 插入流程中，首先会检查是否有空余的位置，通过 `matchEmptyOrDeleted()` 方法，类似匹配 h2 一样，生成一个 bitset，如果此时发现没有空槽，则会直接报错，因为在进入 `putSoltSmall()` 之前检查是有空 Slot 的，现在没有代表发生了并发写入操作。\n3. 找到第一个空槽后，会将 Key 插入，然后预插入 Val，然后将对应的控制字节改为 key 的 h2 值 + 高位 0，代表 Full 状态，最后将 used 数值 + 1，返回 val 的地址，由上层写入。\n（**注意，这里在小 map 中，检查空余元素用的是  `matchEmptyOrDeleted()`  方法，也就是不分 deleted 状态和 empty 状态，这是为什么？因为在一个组内，是不是遇到 empty 就搜索早停的，搜索早停是指在当前组内发现了 empty slot，则可以停止搜索下一组**）  \n如果成功在小 map 中插入了，则会检查是否有并发写冲突，如果有，则报错，如果没有则修改写标记位，并且返回 val 的地址。**而如果没有成功插入，也就是小 map 中没有空槽，则会调用 `growToTable()` 将 group 升级为 table，真正的进化成 SwissTable，接着往下走普通 map 的插入流程。这里的内容暂时不展开，具体见扩容流程（第 5 小节）。**\n```go\nif m.dirLen == 0 {\n\tif m.used \u003c abi.MapGroupSlots {\n\t\t// 组内还有空余 slot\n\t\telem := m.putSlotSmall(typ, hash, key)\n\t\t\t\n\t\tif m.writing == 0 {\n\t\t\tfatal(\"concurrent map writes\")\n\t\t}\n\t\t// 恢复写 flag\n\t\tm.writing ^= 1\n\t\t\n\t\treturn elem\n\t}\n\t\t\n\t// 没有找到空位，则从小 map （group）扩容成 table，后往下执行 table 的插入流程\n\tm.growToTable(typ)\n}\n\t\n\t\nfunc (m *Map) putSlotSmall(typ *abi.MapType, hash uintptr, key unsafe.Pointer) unsafe.Pointer {\n\t// 直接获取组的数组\n\tg := groupReference{\n\t\tdata: m.dirPtr,\n\t}\n\t\n\t// 返回匹配到的 bitset\n\tmatch := g.ctrls().matchH2(h2(hash))\n\t\n\tfor match != 0 {\n\t\ti := match.first()\n\t\t\n\t\tslotKey := g.key(typ, i)\n\t\tif typ.IndirectKey() {\n\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t}\n\t\t// 如果是同一个 key\n\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\tif typ.NeedKeyUpdate() {\n\t\t\t\ttypedmemmove(typ.Key, slotKey, key)\n\t\t\t}\n\t\t\t\n\t\t\tslotElem := g.elem(typ, i)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t}\n\t\t\t// val 的地址\n\t\t\treturn slotElem\n\t\t}\n\t\t// 当前槽没有匹配成功，往下找下一个可能会匹配成功的槽\n\t\tmatch = match.removeFirst()\n\t}\n\t\n\t// 代码走到这里代表 map 中不存在这个 key\n\t// 同理，找到 group 内状态为空的槽，小 map 内没有删除这个状态\n\tmatch = g.ctrls().matchEmptyOrDeleted()\n\tif match == 0 {\n\t\t// 如果小 map 内不存在空槽，则出错，因为在进入这个函数之前校验过是有空槽的\n\t\t// 发生这种情况可能是并发写了\n\t\tfatal(\"small map with no empty slot (concurrent map writes?)\")\n\t\treturn nil\n\t}\n\t// 定位到第一个匹配到的空槽\n\ti := match.first()\n\t\n\t// 获取 key 和 v 的地址\n\tslotKey := g.key(typ, i)\n\tif typ.IndirectKey() {\n\t\tkmem := newobject(typ.Key)\n\t\t*(*unsafe.Pointer)(slotKey) = kmem\n\t\tslotKey = kmem\n\t}\n\t// 写入 key\n\ttypedmemmove(typ.Key, slotKey, key)\n\t\n\tslotElem := g.elem(typ, i)\n\tif typ.IndirectElem() {\n\t\temem := newobject(typ.Elem)\n\t\t*(*unsafe.Pointer)(slotElem) = emem\n\t\tslotElem = emem\n\t}\n\t\n\t// 设置槽对应的控制位为 h2\n\tg.ctrls().set(i, ctrl(h2(hash)))\n\t// kv 数量 + 1\n\tm.used++\n\t// 返回 val 的写入地址，上层写入\n\treturn slotElem\n}\n```\n（4）此时进入大表的插入流程，首先是依据 hash 值的高几位来定要插入的 table，接着会根据 h1 和 group 数创建一个探测序列，指定接下来的探测顺序。\n```go\nidx := m.directoryIndex(hash)\nt := m.directoryAt(idx)\n\t\t\n// 创建探测序列\nseq := makeProbeSeq(h1(hash), t.groups.lengthMask)\n```\n（5）接着按照探测序列逐个 group 进行探测，首先这里会先按照探测序列定位的组，在组内找，是否有一样的 key，如果有，则改为更新操作，并退出探测。否则，往下执行逻辑。\n```go\nfor ; ; seq = seq.next() {\n\tg := t.groups.group(typ, seq.offset)\n\t// 找到 group 内 hash 一致的 slot\n\tmatch := g.ctrls().matchH2(h2Hash)\n\t\n\t// 遍历 bitset，看是否存在一样的 key\n\tfor match != 0 {\n\t\ti := match.first()\n\t\t\n\t\tslotKey := g.key(typ, i)\n\t\tslotKeyOrig := slotKey\n\t\tif typ.IndirectKey() {\n\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t}\n\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\t// 匹配成功，更新 key\n\t\t\tif typ.NeedKeyUpdate() {\n\t\t\t\ttypedmemmove(typ.Key, slotKey, key)\n\t\t\t}\n\t\t\t\n\t\t\tslotElem = unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\tslotElem = *((*unsafe.Pointer)(slotElem))\n\t\t\t}\n\t\t\t\t\n\t\t\tt.checkInvariants(typ, m)\n\t\t\t// 退出\n\t\t\tbreak outer\n\t\t}\n\t\t// 找下一个 group\n\t\tmatch = match.removeFirst()\n\t}\n\t// ...\n```\n（6）如果在组内，没有找到一样的 key，并不是按照探测序列查找下一个组，而是会检查这组内是否有空的槽，如果这组内有空的槽，则不需要往下进一步查找下一个组了（**因为插入不变式，首先插入同一个 Key 一定是走相同的探测序列，而插入是遇到 empty 就会插入，所以后续的组内不可能有这个 key**）。  \n```go\n\t// ...\n\tmatch = g.ctrls().matchEmpty()\n\t// 注意这里是 if match 不是 for match\n\tif match != 0 {\n\t\t// 找到空槽意味着到达了探测序列的终点\n\t\tvar i uintptr\n\t\t// 如果之前有墓碑位，则使用墓碑位插入\n\t\tif firstDeletedGroup.data != nil {\n\t\t\tg = firstDeletedGroup\n\t\t\ti = firstDeletedSlot\n\t\t\t// 等于暂时将墓碑位所占据的位置释放，\n\t\t\t// 逻辑上就是 table 多了一个能插入的位置\n\t\t\tt.growthLeft++ // will be decremented below to become a no-op.\n\t\t\t} else {\n\t\t\t\t// Otherwise, use the empty slot.\n\t\t\t\t// 如果没有墓碑槽，这里直接用空槽\n\t\t\t\ti = match.first()\n\t\t\t}\n```\n此时，如果查到该组内有 empty 槽，则会检查在之前探测的组内有没有遇到 deleted 槽（墓碑位），如果有，则会**尝试**将数据插在 deleted 槽中，复用槽。否则，则会**尝试**插入到 empty 槽（注意是尝试）。插入之前会校验当前组内的要不要超负载，如果超过负载，会执行 table 扩容，然后扩容后，再重新探测插入（扩容的逻辑在第 5 小节）。\n```go\n\t// ...\n\t\t// 没超过负载，代表还有插入空间 \n\t\t// （growthLeft 是指在 rehash 之前还能插入多少 kv 对）\n\t\tif t.growthLeft \u003e 0 {\n\t\t\tslotKey := g.key(typ, i)\n\t\t\tslotKeyOrig := slotKey\n\t\t\tif typ.IndirectKey() {\n\t\t\t\tkmem := newobject(typ.Key)\n\t\t\t\t*(*unsafe.Pointer)(slotKey) = kmem\n\t\t\t\tslotKey = kmem\n\t\t\t}\n\t\t\ttypedmemmove(typ.Key, slotKey, key)\n\t\t\tslotElem = unsafe.Pointer(uintptr(slotKeyOrig) + typ.ElemOff)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\temem := newobject(typ.Elem)\n\t\t\t\t*(*unsafe.Pointer)(slotElem) = emem\n\t\t\t\tslotElem = emem\n\t\t\t}\n\t\t\t// 设置 slot 对应的控制字节，可插入位置 --\n\t\t\tg.ctrls().set(i, ctrl(h2Hash))\n\t\t\t// 空位 --\n\t\t\tt.growthLeft--\n\t\t\t// 使用位 ++\n\t\t\tt.used++\n\t\t\tm.used++\n\t\t\t// 退出\n\t\t\tt.checkInvariants(typ, m)\n\t\t\tbreak outer\n\t\t}\n\t\t// 如果超负载了，则执行扩容\n\t\tt.rehash(typ, m)\n\t\t// 因为 rehash 了，所以直接重新走一遍插入流程\n\t\tcontinue outer\n\t}\n```\n假设如果在当前组内没有找到 empty 槽，那代表可以继续往下一组探测了，探测之前会看看之前有没有记录到的墓碑位，如果有就不用再检查当前组内有没有墓碑了，如果没记录过，还要检查一下当前组内有没有墓碑位，主要是为了复用 deleted 槽。\n```go\n// 这里尝试找有没有墓碑（delete 槽）\n// 如果找到，则将 delete 槽赋值给变量，后面会将元素插入该位置\n\tif firstDeletedGroup.data == nil {\n\t\t// Since we already checked for empty slots\n\t\t// above, matches here must be deleted slots.\n\t\tmatch = g.ctrls().matchEmptyOrDeleted()\n\t\tif match != 0 {\n\t\t\tfirstDeletedGroup = g\n\t\t\tfirstDeletedSlot = match.first()\n\t\t}\n\t}\n```\n（7） 插入完成后，校验写标记有没有被破坏，如果有，则抛出错误，如果没有，则重置写标记位，并且返回 val 的地址，由上层插入。\n```go\n// 检查写状态有没有被破坏\nif m.writing == 0 {\n\tfatal(\"concurrent map writes\")\n}\nm.writing ^= 1\n// 返回插入位置\nreturn slotElem\n```\n### 3.2.4 删除\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105172301192.png)\n\n删除操作相对来说简单很多，因为 map 只会扩容，不会缩容，所以基本上就是一个查找到 kv 并且置为 deleted 状态的过程。让我们往下看\n```go\nfunc (m *Map) Delete(typ *abi.MapType, key unsafe.Pointer) {\n\t// 如果 map nil 或 空，直接返回\n\tif m == nil || m.Used() == 0 {\n\t\tif err := mapKeyError(typ, key); err != nil {\n\t\t\tpanic(err) // see issue 23734\n\t\t}\n\t\treturn\n\t}\n\n\t// 并发写检查\n\tif m.writing != 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\n\t// 计算 hash 并设置写标记\n\thash := typ.Hasher(key, m.seed)\n\n\t// Set writing after calling Hasher, since Hasher may panic, in which\n\t// case we have not actually done a write.\n\tm.writing ^= 1 // toggle, see comment on writing\n\n\t// 小表直接去 group 内删除\n\tif m.dirLen == 0 {\n\t\tm.deleteSmall(typ, hash, key)\n\t} else {\n\t\t// 定位 table\n\t\tidx := m.directoryIndex(hash)\n\t\t// table 删除\n\t\tif m.directoryAt(idx).Delete(typ, m, hash, key) {\n\t\t\tm.tombstonePossible = true\n\t\t}\n\t}\n\n\t// 如果此时删完之后 map 清零了，则重置 hash 种子\n\tif m.used == 0 {\n\t\t// Reset the hash seed to make it more difficult for attackers\n\t\t// to repeatedly trigger hash collisions. See\n\t\t// https://go.dev/issue/25237.\n\t\tm.seed = uintptr(rand())\n\t}\n\n\t// 并发写检测\n\tif m.writing == 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\t// 重置 flag 位\n\tm.writing ^= 1\n}\n```\n（1）如果 map 为 nil 或空，则直接返回，否则进行并发写检查，计算 hash 值，设置写标记位\n```go\nif m == nil || m.Used() == 0 {\n\tif err := mapKeyError(typ, key); err != nil {\n\t\tpanic(err) // see issue 23734\n\t}\n\treturn\n}\n\n// 并发写检查\nif m.writing != 0 {\n\tfatal(\"concurrent map writes\")\n}\n\t\n// 计算 hash 并设置写标记\nhash := typ.Hasher(key, m.seed)\n\t\nm.writing ^= 1 // toggle, see comment on writing\n```\n（2）接着要进入删除流程，这里也分在小 map 中删除（`m.deleteSmall(typ, hash, key)`）和在普通 map 中删除（`m.directoryAt(idx).Delete(typ, m, hash, key)`）\n```go\n// 小表直接去 group 内删除\nif m.dirLen == 0 {\n\tm.deleteSmall(typ, hash, key)\n} else {\n\t// 定位 table\n\tidx := m.directoryIndex(hash)\n\t// table 删除\n\tif m.directoryAt(idx).Delete(typ, m, hash, key) {\n\t\tm.tombstonePossible = true\n\t}\n}\n```\n**小 map 删除流程：**\n```go\nfunc (m *Map) deleteSmall(typ *abi.MapType, hash uintptr, key unsafe.Pointer) {\n\t// 获取 group 地址\n\tg := groupReference{\n\t\tdata: m.dirPtr,\n\t}\n\n\t// 找到组内匹配到的 slot\n\tmatch := g.ctrls().matchH2(h2(hash))\n\n\t// 遍历 group 内的\n\tfor match != 0 {\n\t\ti := match.first()\n\t\tslotKey := g.key(typ, i)\n\t\torigSlotKey := slotKey\n\t\tif typ.IndirectKey() {\n\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t}\n\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\t// slot 位使用数 --\n\t\t\tm.used--\n\t\t\t// 清除 key\n\t\t\tif typ.IndirectKey() {\n\t\t\t\t// Clearing the pointer is sufficient.\n\t\t\t\t*(*unsafe.Pointer)(origSlotKey) = nil\n\t\t\t} else if typ.Key.Pointers() {\n\t\t\t\t// Only bother clearing if there are pointers.\n\t\t\t\ttypedmemclr(typ.Key, slotKey)\n\t\t\t}\n\t\t\t// 清除 val\n\t\t\tslotElem := g.elem(typ, i)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\t// Clearing the pointer is sufficient.\n\t\t\t\t*(*unsafe.Pointer)(slotElem) = nil\n\t\t\t} else {\n\t\t\t\t// Unlike keys, always clear the elem (even if\n\t\t\t\t// it contains no pointers), as compound\n\t\t\t\t// assignment operations depend on cleared\n\t\t\t\t// deleted values. See\n\t\t\t\t// https://go.dev/issue/25936.\n\t\t\t\ttypedmemclr(typ.Elem, slotElem)\n\t\t\t}\n\n\t\t\t// We only have 1 group, so it is OK to immediately\n\t\t\t// reuse deleted slots.\n\t\t\t// 设置控制字节为 Empty\n\t\t\tg.ctrls().set(i, ctrlEmpty)\n\t\t\treturn\n\t\t}\n\t\t// 没找到，往下找下一个匹配到的 slot\n\t\tmatch = match.removeFirst()\n\t}\n}\n```\n小 map 的删除流程和查找无异，但是要注意，小 map 中因为只有一个 group，不涉及到探测序列，所以其无需维护 deleted 位，所以删除只需要置为 empty 即可。  \n**普通 map 删除流程：**  \n```go\nfunc (t *table) Delete(typ *abi.MapType, m *Map, hash uintptr, key unsafe.Pointer) bool {\n\t// 初始化探测序列\n\tseq := makeProbeSeq(h1(hash), t.groups.lengthMask)\n\t// h2\n\th2Hash := h2(hash)\n\t// 按照序列探测\n\tfor ; ; seq = seq.next() {\n\t\t// 定位组\n\t\tg := t.groups.group(typ, seq.offset)\n\t\t// 组内匹配的 slot\n\t\tmatch := g.ctrls().matchH2(h2Hash)\n\t\t\n\t\t// 如果有初步匹配的key，则进一步对比 key\n\t\tfor match != 0 {\n\t\t\ti := match.first()\n\t\t\t\n\t\t\tslotKey := g.key(typ, i)\n\t\t\torigSlotKey := slotKey\n\t\t\tif typ.IndirectKey() {\n\t\t\t\tslotKey = *((*unsafe.Pointer)(slotKey))\n\t\t\t}\n\t\t\t\n\t\t\tif typ.Key.Equal(key, slotKey) {\n\t\t\t\t// key 值相等，槽位++\n\t\t\t\tt.used--\n\t\t\t\tm.used--\n\t\t\t\t// 移除 kv\n\t\t\t\tif typ.IndirectKey() {\n\t\t\t\t\t*(*unsafe.Pointer)(origSlotKey) = nil\n\t\t\t\t} else if typ.Key.Pointers() {\n\t\t\t\t\ttypedmemclr(typ.Key, slotKey)\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tslotElem := g.elem(typ, i)\n\t\t\t\tif typ.IndirectElem() {\n\t\t\t\t\t// Clearing the pointer is sufficient.\n\t\t\t\t\t*(*unsafe.Pointer)(slotElem) = nil\n\t\t\t\t} else {\n\t\t\t\t\ttypedmemclr(typ.Elem, slotElem)\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tvar tombstone bool\n\t\t\t\t// 组内有空位，则将这一位置为 empty 没关系\n\t\t\t\tif g.ctrls().matchEmpty() != 0 {\n\t\t\t\t\tg.ctrls().set(i, ctrlEmpty)\n\t\t\t\t\tt.growthLeft++\n\t\t\t\t} else {\n\t\t\t\t\t// 组内没有空位，则这位必须置为 delete，不然探测序列会被打断\n\t\t\t\t\tg.ctrls().set(i, ctrlDeleted)\n\t\t\t\t\ttombstone = true\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tt.checkInvariants(typ, m)\n\t\t\t\treturn tombstone\n\t\t\t}\n\t\t\tmatch = match.removeFirst()\n\t\t}\n\t\t\n\t\t// 看组内是否有 empty 槽，如果有可以直接返回了，代表探测序列终止\n\t\tmatch = g.ctrls().matchEmpty()\n\t\tif match != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n}\n```\n对于普通 map 删除而言，是需要维护 deleted 位的，其本质也和查找类似，先计算出探测序列，然后依据探测序列一组一组的查找，知道查到某个组内包含该 Key 或者这个组内有 Empty 槽。\n**注意找到元素删除的时候，状态位的设置也是分情况的：**\n1. 如果 key 所在的组内有 empty 状态的 slot，那么这个位置可以直接设置为 empty，因为如果有后续的其他 key 查找经过该组，本身就会因为原先的 empty 状态导致早停，所以这里将这个空槽直接置为 empty 并不会影响经过该组的探测序列的探测。\n2. 如果 key 所在的组内没有 empty 状态的 slot，那么这里就需要设置为 deleted，从而维护其他探测序列的正常探测。\n（3）如果在某个组内没有找到 key，但是发现了 empty，则直接早停返回\n```go\n// 看组内是否有 empty 槽，如果有可以直接返回了，代表探测序列终止\nmatch = g.ctrls().matchEmpty()\nif match != 0 {\n\treturn false\n}\n```\n### 3.2.5 扩容\n扩容是 map 中比较重要的部分，他会涉及到性能问题。在 1.24 后的 go 中，map 的扩容主要分为三种情况：\n1. 小 map 单 group 扩容为 table\n2. table 横向扩容（单 table 内的 group 数翻倍）\n3. table 纵向分裂扩容（单 table 内 group 数已到达上限，分裂成两个 table）\n#### 小 map 单 group 扩容为 table\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105162303533.png)\n\n我们知道一开始预估容量小于 8 个 slot 的时候，我们会只给 map 分配一个 group，假设此时 8 个 slot 不够用了，则会让 group 升级为 table。\n```go\nfunc (m *Map) growToTable(typ *abi.MapType) {\n\t// 创建一个大小是原来两倍的 table（原来的小表 slot 只有 8）\n\ttab := newTable(typ, 2*abi.MapGroupSlots, 0, 0)\n\t\n\tg := groupReference{\n\t\tdata: m.dirPtr,\n\t}\n\t\n\t// 从小 map 中迁移旧数据\n\tfor i := uintptr(0); i \u003c abi.MapGroupSlots; i++ {\n\t\tif (g.ctrls().get(i) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t\t// 遇到空槽跳过\n\t\t\t// Empty\n\t\t\tcontinue\n\t\t}\n\t\t\n\t\t// 获取 key\n\t\tkey := g.key(typ, i)\n\t\tif typ.IndirectKey() {\n\t\t\tkey = *((*unsafe.Pointer)(key))\n\t\t}\n\t\t\n\t\t// 获取值\n\t\telem := g.elem(typ, i)\n\t\tif typ.IndirectElem() {\n\t\t\telem = *((*unsafe.Pointer)(elem))\n\t\t}\n\t\t// 按照 hash 求出这个 key 的 hash 值\n\t\thash := typ.Hasher(key, m.seed)\n\t\t\n\t\t// 在 table 中找到合适的位置插入元素\n\t\ttab.uncheckedPutSlot(typ, hash, key, elem)\n\t}\n\t\n\t// 创建 dir 数组\n\tdirectory := make([]*table, 1)\n\t\n\tdirectory[0] = tab\n\t\n\t// dirptr 指向 tabel\n\tm.dirPtr = unsafe.Pointer(\u0026directory[0])\n\tm.dirLen = len(directory)\n\t\n\t// 此时 Depth 为 0，因为只有一个 Table，不需要任何 bit 位来判断插哪个表\n\tm.globalDepth = 0\n\tm.globalShift = depthToShift(m.globalDepth)\n}\n```\n（1）创建一个 16 个 slot 的 table，也就是 2 group。\n```go\ntab := newTable(typ, 2*abi.MapGroupSlots, 0, 0)\n```\n（2）迁移原来的 group 中的 kv 对到新 table 中。这里就是组内的遍历和直接插入。\n```go\ng := groupReference{\n\tdata: m.dirPtr,\n}\n\t\n// 从小 map 中迁移旧数据\nfor i := uintptr(0); i \u003c abi.MapGroupSlots; i++ {\n\tif (g.ctrls().get(i) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t// 遇到空槽跳过\n\t\t// Empty\n\t\tcontinue\n\t}\n\t\t\n\t// 获取 key\n\tkey := g.key(typ, i)\n\tif typ.IndirectKey() {\n\t\tkey = *((*unsafe.Pointer)(key))\n\t}\n\t\t\n\t// 获取值\n\telem := g.elem(typ, i)\n\tif typ.IndirectElem() {\n\t\telem = *((*unsafe.Pointer)(elem))\n\t}\n\t// 按照 hash 求出这个 key 的 hash 值\n\thash := typ.Hasher(key, m.seed)\n\t\t\n\t// 在 table 中找到合适的位置插入元素\n\ttab.uncheckedPutSlot(typ, hash, key, elem)\n}\n```\n（3）更新 Map 的信息，首先是重新设置 dir 数组，因为原来的小 map 的 dir 指针指向的是一个 group，现在指向一个 table 数组。接着需要设置全局目录的深度，就是在定位一个 table 的时候，需要取 hash 的高多少位。\n```go\n// 创建 dir 数组\ndirectory := make([]*table, 1)\n\t\ndirectory[0] = tab\n\t\n// dirptr 指向 tabel\nm.dirPtr = unsafe.Pointer(\u0026directory[0])\nm.dirLen = len(directory)\n\t\n// 此时 Depth 为 0，因为只有一个 Table，不需要任何 bit 位来判断插哪个表\nm.globalDepth = 0\nm.globalShift = depthToShift(m.globalDepth)\n```\n#### table 横向扩容\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105164045386.png)\n\ntable 的横向扩容是当插入元素到 table 中，此时 table 在插入元素就要超过负载了，但是此时 table 中的组数还没有到达上限，那么此时 table 就会横向的增加 group 数，从而实现扩容。\n```go\nfunc (t *table) grow(typ *abi.MapType, m *Map, newCapacity uint16) {\n\t// 此时发生横向的扩容\n\t// 直接分配一个新 table，以代替原有 table\n\tnewTable := newTable(typ, uint64(newCapacity), t.index, t.localDepth)\n\t\n\tif t.capacity \u003e 0 {\n\t\t// 遍历原表中的所有 group\n\t\tfor i := uint64(0); i \u003c= t.groups.lengthMask; i++ {\n\t\t\tg := t.groups.group(typ, i)\n\t\t\t// 遍历 group 中的所有 slot\n\t\t\tfor j := uintptr(0); j \u003c abi.MapGroupSlots; j++ {\n\t\t\t\tif (g.ctrls().get(j) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t\t\t\t// Empty or deleted\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tkey := g.key(typ, j)\n\t\t\t\tif typ.IndirectKey() {\n\t\t\t\t\tkey = *((*unsafe.Pointer)(key))\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\telem := g.elem(typ, j)\n\t\t\t\tif typ.IndirectElem() {\n\t\t\t\t\telem = *((*unsafe.Pointer)(elem))\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\thash := typ.Hasher(key, m.seed)\n\t\t\t\t// rehash 插入\n\t\t\t\tnewTable.uncheckedPutSlot(typ, hash, key, elem)\n\t\t\t}\n\t\t}\n\t}\n\t// 更换 table\n\tnewTable.checkInvariants(typ, m)\n\tm.replaceTable(newTable)\n\t// 把旧表标记废弃\n\tt.index = -1\n}\n```\n（1）首先进入横向扩容的条件是，当前的组数，没有超过最大限制的 1/2，因为扩容是翻倍扩容，所以当 `2 * t.capacity \u003c 1024` 的时候，会进入 `grow()`\n```go\nfunc (t *table) rehash(typ *abi.MapType, m *Map) {\n\t// 新的 table 容量先预设为原来容量的一倍\n\tnewCapacity := 2 * t.capacity\n\t// 如果新容量没有超过单表槽数的最大值\n\tif newCapacity \u003c= maxTableCapacity {\n\t\t// table 横向扩容\n\t\tt.grow(typ, m, newCapacity)\n\t\treturn\n\t}\n\t// table 纵向分裂扩容\n\tt.split(typ, m)\n}\n```\n（2）分配一个新的 table，因为需要 SwissTable 需要保障内存的连续性，所以每次扩容都需要重新申请空间。而因为没有增加 table，所以 index 和 localDepth 不会改变。\n```go\nnewTable := newTable(typ, uint64(newCapacity), t.index, t.localDepth)\n```\n（3）遍历原 table 中所有 group，将 table 中的所有 kv rehash 到新 table 中，从而实现扩容。\n```go\nif t.capacity \u003e 0 {\n\t// 遍历原表中的所有 group\n\tfor i := uint64(0); i \u003c= t.groups.lengthMask; i++ {\n\t\tg := t.groups.group(typ, i)\n\t\t// 遍历 group 中的所有 slot\n\t\tfor j := uintptr(0); j \u003c abi.MapGroupSlots; j++ {\n\t\t\tif (g.ctrls().get(j) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t\t\t// Empty or deleted\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t\t\n\t\t\tkey := g.key(typ, j)\n\t\t\tif typ.IndirectKey() {\n\t\t\t\tkey = *((*unsafe.Pointer)(key))\n\t\t\t}\n\t\t\t\t\n\t\t\telem := g.elem(typ, j)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\telem = *((*unsafe.Pointer)(elem))\n\t\t\t}\n\t\t\t\t\n\t\t\thash := typ.Hasher(key, m.seed)\n\t\t\t// rehash 插入\n\t\t\tnewTable.uncheckedPutSlot(typ, hash, key, elem)\n\t\t}\n\t}\n}\n```\n（4）在 Map 中重新设置目录的指针，指向新的 table\n```go\n// 更换 table\nnewTable.checkInvariants(typ, m)\nm.replaceTable(newTable)\n// 把旧表标记废弃\nt.index = -1\n```\n#### table 纵向分裂扩容\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105164510168.png)\n\n当 table 中的的组数再扩大就要超出单 table 的限制了，此时就会触发纵向的分裂扩容，table 数量增加，会进而影响到 Map 中更多的字段。\n```go\nfunc (t *table) split(typ *abi.MapType, m *Map) {\n\t// 此时发生纵向扩容，也就是说 table 数量会 + 1\n\t// 所以 localDepth + 1\n\tlocalDepth := t.localDepth\n\tlocalDepth++\n\t\n\t// TODO: is this the best capacity?\n\t// 创建两个 1024 的 table\n\tleft := newTable(typ, maxTableCapacity, -1, localDepth)\n\tright := newTable(typ, maxTableCapacity, -1, localDepth)\n\t\n\t// Split in half at the localDepth bit from the top.\n\tmask := localDepthMask(localDepth)\n\t\n\t// 原 table 中的 kv rehash\n\tfor i := uint64(0); i \u003c= t.groups.lengthMask; i++ {\n\t\tg := t.groups.group(typ, i)\n\t\tfor j := uintptr(0); j \u003c abi.MapGroupSlots; j++ {\n\t\t\tif (g.ctrls().get(j) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t\t\t// Empty or deleted\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t\n\t\t\tkey := g.key(typ, j)\n\t\t\tif typ.IndirectKey() {\n\t\t\t\tkey = *((*unsafe.Pointer)(key))\n\t\t\t}\n\t\t\t\n\t\t\telem := g.elem(typ, j)\n\t\t\tif typ.IndirectElem() {\n\t\t\t\telem = *((*unsafe.Pointer)(elem))\n\t\t\t}\n\t\t\t\n\t\t\thash := typ.Hasher(key, m.seed)\n\t\t\tvar newTable *table\n\t\t\t// 按 localDepth 的那一位决定左/右目标表\n\t\t\tif hash\u0026mask == 0 {\n\t\t\t\tnewTable = left\n\t\t\t} else {\n\t\t\t\tnewTable = right\n\t\t\t}\n\t\t\tnewTable.uncheckedPutSlot(typ, hash, key, elem)\n\t\t}\n\t}\n\t// 在 Map 中注册两个表\n\tm.installTableSplit(t, left, right)\n\t// 原表弃用\n\tt.index = -1\n}\n```\n（1）首先因为表的拆分，所以 localDepth 会增加，因为 localDepth 代表单表的深度。举个例子可以更好的理解这个字段。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105103102428.png)\n\n假设当前 map 中是如上情况，globalDepth = 1，则代表需要 hash 值高 1 位就能定位选择那个 table。当高位为 0 时，则定位 table1，否则定位 table2。而 localDepth = 1 代表的则是只需要一位就能确定这个 tabel。**因为随着其他 table 的扩容，可能会导致 globalDepth 增加，但是没分裂的table 的 localDepth 不会改变，那对于没分裂的表，只需要 localDepth 就能确定。看下面的例子。** 假设现在 Table1 分裂了。\n![image.png](https://blog-pic-1304506649.cos.ap-shanghai.myqcloud.com/20251105104040576.png)\n此时在 map 中有 table1 和 table3（table1 分裂出的 table）以及 table2，但是因为目录是成倍扩容的，也就是说 dir 长度为 4。此时目录项 1 和 2 分别指向 tabel1 和 table3，而目录项 3 和 4 都指向 table2，也就是说区分 table1 和 table3 需要高两位。**例如00 分去 table1，01 分去tabel3，而 10 和 11 都分去 table2，这就是 localDepth 的意义。实际上就是虽然全局目录会有很多页，但是因为有的 table 不扩容，随着页目录增长，随有很多目录项都指向同一个 table，所以这些目录项所代表的低位是没有参考价值的，只需要高 localDepth 位就能确定其所在的 table。**\n（2）拆分 table，创建两个最大的 table，分别作为拆分后的两个 table。\n```go\nleft := newTable(typ, maxTableCapacity, -1, localDepth)\nright := newTable(typ, maxTableCapacity, -1, localDepth)\n```\n（3）rehash 原来表中的数据到新的两个表中，用高 localDepth 的最低位判断分去哪个 table 中。\n```go\nmask := localDepthMask(localDepth)\n\nfor i := uint64(0); i \u003c= t.groups.lengthMask; i++ {\n\tg := t.groups.group(typ, i)\n\tfor j := uintptr(0); j \u003c abi.MapGroupSlots; j++ {\n\t\tif (g.ctrls().get(j) \u0026 ctrlEmpty) == ctrlEmpty {\n\t\t\t// Empty or deleted\n\t\t\tcontinue\n\t\t}\n\t\t\n\t\tkey := g.key(typ, j)\n\t\tif typ.IndirectKey() {\n\t\t\tkey = *((*unsafe.Pointer)(key))\n\t\t}\n\t\t\t\n\t\telem := g.elem(typ, j)\n\t\tif typ.IndirectElem() {\n\t\t\telem = *((*unsafe.Pointer)(elem))\n\t\t}\n\t\t\t\n\t\thash := typ.Hasher(key, m.seed)\n\t\tvar newTable *table\n\t\t// 按 localDepth 的那一位决定左/右目标表\n\t\tif hash\u0026mask == 0 {\n\t\t\tnewTable = left\n\t\t} else {\n\t\t\tnewTable = right\n\t\t}\n\t\tnewTable.uncheckedPutSlot(typ, hash, key, elem)\n\t}\n}\n```\n（4）在 Map 中注册这两个 table，并且弃用原表。因为 table 增加，所以 Map 中的表目录也扩大，表目录指针的指向也要改变。\n```go\n// 在 Map 中注册两个表\nm.installTableSplit(t, left, right)\n// 原表弃用\nt.index = -1\n\nfunc (m *Map) installTableSplit(old, left, right *table) {\n\t// 目录需要扩充\n\tif old.localDepth == m.globalDepth {\n\t\t// 如果全局的表深度和原来的表一致，那就代表深度不够了，需要重新增加\n\t\tnewDir := make([]*table, m.dirLen*2)\n\t\tfor i := range m.dirLen {\n\t\t\tt := m.directoryAt(uintptr(i))\n\t\t\t// 每个旧入口复制成两个相邻的新入口\n\t\t\tnewDir[2*i] = t\n\t\t\tnewDir[2*i+1] = t\n\t\t\t// t 可能本来就被多个目录项引用，更新 table 的 idx\n\t\t\tif t.index == i {\n\t\t\t\tt.index = 2 * i\n\t\t\t}\n\t\t}\n\t\t// 全局深度增加\n\t\tm.globalDepth++\n\t\tm.globalShift--\n\t\t//m.directory = newDir\n\t\tm.dirPtr = unsafe.Pointer(\u0026newDir[0])\n\t\tm.dirLen = len(newDir)\n\t}\n\t// 不需要扩充目录\n\t// left 的 idx 是旧表的 idx\n\tleft.index = old.index\n\tm.replaceTable(left)\n\t// 同理\n\tentries := 1 \u003c\u003c (m.globalDepth - left.localDepth)\n\tright.index = left.index + entries\n\tm.replaceTable(right)\n}\n```\n首先会判断当前这个表的 localDepth 有没有超过 Map 的globalDepth，如果超过，就代表 dir 不够用了，需要重新分配空间，并且重新分配指针指向；否则不需要扩容dir。例如上面那个例子，假设此时 table2 要继续split，那么并不需要扩容 dir，只需要将 table2 所占用的第一个目录指针指向分裂后的第一个新表，第二个指针指向分裂后的第二个新表即可。  \n### 3.2.6 迭代（TODO）\n迭代感觉 go 团队都还没想好怎么做，目前只是初始版本啊.......\n# 4 彩蛋\n## 4.1 这里为什么 go 没有采用一个组 16 个槽，一步提高对比的效率呢？\n我认为 go 是为了移植性和初步实验，底层采用的应该不是 SIMD 并行比对的，而是使用了另一种并行方案——寄存器内并行，我们现在的平台基本上都是 64 位平台，也就是说寄存器是 64 位，这样的情况下，go 的这套代码不需要指令支持，只需要寄存器内并行即可计算，当然后续可能会采用 SIMD 提高效率，因为 go 现在的 map 代码中存在大量的 TODO；而且也有人发现，在某些场景下，SwissTable 的效率远不如原来的拉链法，所以慢慢等看 go 的团队如何做抉择吧。\n# 参考资料\n1. https://www.youtube.com/watch?v=ncHmEUmJZf4\n2. https://www.bilibili.com/video/BV1iRxjehEFM/?spm_id_from=333.1391.0.0\u0026vd_source=ec3e6adc144c0dda701e75fe9d38fa57\n3. https://www.cnblogs.com/apocelipes/p/17562468.html\n4. https://zhuanlan.zhihu.com/p/29295935067\n5. https://github.com/golang/go/issues/54766","date":"2025-10-31","path":"/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/go/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-go-map%E4%B8%8B/","permalink":"http://xiaoshidui.top/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/go/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-go-map%E4%B8%8B/","title":"深入浅出 Go-Map（下）"},{"content":"\n    1.24 前后 map 的世界，在 1.24 后，go 的 map 换了实现方式，让我们来康康\n\n# 1 追踪 map 的起始点\n\n看下面这段代码，我们来定位一下 make 之后发生了什么？\n\n```go\npackage main\n\nfunc main() {\n\t_ = make(map[string]int, 16)\n}\n```\n\n使用 `go tool compile -S` 编译文件，可以发现 map 实际上是定位到了底层的 runtime.makemap 中，这使得我们有了窥探 map 源码的机会。\n\n```shell\n❯ go tool compile -S main.go | grep 'make'\n    0x0034 00052 (.../main.go:4)        CALL    runtime.makemap(SB)\n    rel 52+4 t=R_CALLARM64 runtime.makemap+0\n```\n\n在进入具体方法之前，我们先了解一下 1.24 之前 go 的 map 的结构\n\n# 2 1.24 前的世界\n\n    本节 go 的代码是基于 1.23.2 版本的\n\n## 2.1 map 的基本构造\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251029174955534.png)\n在 1.24 前，go 的 map 实现是基于桶数组 + 溢出桶实现的，具体而言是基于 runtime 下的这两个结构体实现的。  \n首先是 hmap，他是宏观上的 map，包含了 map 的键值对数量、状态、桶的数量以及桶的指针等等。当我们创建了一个 map 的时候，宏观来看就是创建 hmap。\n\n```go\ntype hmap struct {\n\t// map 中键值对的数量\n\tcount     int\n\t// 映射到上面的 const 中，用来标记当前 map 的状态\n\tflags     uint8\n\t// 桶的数量 数量为 2^B\n\tB         uint8\n\tnoverflow uint16\n\t// 随机 hash 种子\n\thash0     uint32 // hash seed\n\t// unsafe.Pointer 是指向任意地址的一个指针\n\t// 当前桶数组\n\tbuckets    unsafe.Pointer\n\t// 旧桶数组\n\toldbuckets unsafe.Pointer\n\t// 记录当前已经迁移完的旧桶数量 nevacuate = 2 ^ (B - 1) 时迁移完毕\n\tnevacuate  uintptr\n\tclearSeq   uint64\n\textra *mapextra // optional fields\n}\n```\n\n微观而言，让我们来看下面这个结构体。这个结构体是什么？这个结构体就是一个溢出桶，这里很奇怪的就是明明溢出桶里应该存放的是 8 个 KV 对，怎么这里只有一个 tophash 的数组？  \n![](https://img.xiaoshidui.top/blog-pic/images/20251030222416534.png)\n原因如下：**go 早期是不支持泛型的，当你创建任意类型的 map 的时候，需要编译器去做类型判断才能内存分配（如果用 `interface{}` 会怎么样？—— 文章末尾揭晓彩蛋），所以当编译器在推断出传入的类型后，会在这个结构体的内存后紧挨的规划出一片内存，用来存放 Key、Val 以及溢出桶指针。虽然整块内存都属于 bmap，但是 bmap 这个结构体只表述了头部的一部分内存，实际上的内存由编译器决定。**\n\n```go\n// A bucket for a Go map.\ntype bmap struct {\n\t// abi.OldMapBucketCount = 1 \u003e\u003e 3\n\t// hash 值的高八位\n\ttophash [abi.OldMapBucketCount]uint8\n}\n// 扩展后，可以推断出逻辑上的 bmap 长下面这样\ntype bmap struct {\n\ttophash [abi.OldMapBucketCount]uint8\n\tkey [abi.OldMapBucketCount]keyType\n\tval [abi.OldMapBucketCount]valueType\n\tnextBmap *bmap\n}\n```\n\n接着是 extra 字段\n\n```go\ntype mapextra struct {\n\t// 存放当前 map 下所有的溢出桶的指针\n\toverflow    *[]*bmap\n\t// 扩容迁移时，旧的溢出桶指针存放地址\n\toldoverflow *[]*bmap\n\t// 额外创建一些 bmap 缓存起来供给\n\tnextOverflow *bmap\n}\n```\n\n## 2.2 map 的常规操作\n\n下面我们从四个 map 的常见操作走进基于 桶 + 溢出桶 的实现。\n\n### 2.2.1 创建\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030225101066.png)\n\n当我们创建一个 map 的时候，殊途同归都会走向下面 `runtime.makemap()` 的方法。不妨追踪一下\n\n```go\nfunc main() {\n\t_ = make(map[string]int, 8)\n}\n```\n\n使用 dlv 工具可以看到最后走向了 `runtime.makemap`，我们还可以看到具体的传参，参数具体的细节我们后面再说。\n\n```shell\n❯ dlv debug main.go\nType 'help' for list of commands.\n(dlv) break runtime.makemap\nBreakpoint 1 set at 0x102848a7c for runtime.makemap() /usr/local/go/src/runtime/map.go:318\n(dlv) continue\n\u003e [Breakpoint 1] runtime.makemap() /usr/local/go/src/runtime/map.go:318 (hits goroutine(1):1 total:1) (PC: 0x102848a7c)\nWarning: debugging optimized function\n   313: //\n   314: // Do not remove or change the type signature.\n   315: // See go.dev/issue/67401.\n   316: //\n   317: //go:linkname makemap\n=\u003e 318: func makemap(t *maptype, hint int, h *hmap) *hmap {\n   319:         mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)\n   320:         if overflow || mem \u003e maxAlloc {\n   321:                 hint = 0\n   322:         }\n   323:\n(dlv) args\nt = (\"*internal/abi.MapType\")(0x102867ac0)\nhint = 16\nh = (*runtime.hmap)(0x14000052728)\n~r0 = (unreadable empty OP stack)\n```\n\n既然定位到了具体的方法，就让我们继续往下看方法的实现吧。\n\n```go\n//go:linkname makemap\nfunc makemap(t *maptype, hint int, h *hmap) *hmap {\n\tmem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)\n\tif overflow || mem \u003e maxAlloc {\n\t\thint = 0\n\t}\n\n\t// initialize Hmap\n\tif h == nil {\n\t\th = new(hmap)\n\t}\n\th.hash0 = uint32(rand())\n\n\t// Find the size parameter B which will hold the requested # of elements.\n\t// For hint \u003c 0 overLoadFactor returns false since hint \u003c bucketCnt.\n\tB := uint8(0)\n\tfor overLoadFactor(hint, B) {\n\t\tB++\n\t}\n\th.B = B\n\n\t// allocate initial hash table\n\t// if B == 0, the buckets field is allocated lazily later (in mapassign)\n\t// If hint is large zeroing this memory could take a while.\n\tif h.B != 0 {\n\t\tvar nextOverflow *bmap\n\t\th.buckets, nextOverflow = makeBucketArray(t, h.B, nil)\n\t\tif nextOverflow != nil {\n\t\t\th.extra = new(mapextra)\n\t\t\th.extra.nextOverflow = nextOverflow\n\t\t}\n\t}\n\n\treturn h\n}\n```\n\n参数 1： t \\*mapType 实际上是一个结构体，他主要包含了 map 本身的类型、键/值的类型、桶的类型、Key 类型占据的字节数、Val 类型占据的字节数以及 一个 Bucket 整体占据的字节数等。多说无益，我们可以打印出来分析一下。\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251029190746936.png)\n我们可以主要关注一下 KeySize、ValueSize 以及 BucketSize，他们分别对应了 String 的大小(16B)、Int 的大小(8B)，以及给一个 bucket 分配的内存的大小((8 + 16 _ 8 + 8 _ 8 + 8) B)。\n参数 2：hint int 是指当我们使用 make 创建 map 时传入的容量提示\n\n```shell\n(dlv) print hint\n16\n```\n\n参数 3：h \\*hmap 实际上是一个可选的目标 hmap，调用方可以传入 hmap 对象给函数使用，因为我们的 map 本身就是一个 hmap 嘛。\n\n```shell\n(dlv) print *h\nruntime.hmap {\n        count: 0,\n        flags: 0,\n        B: 0,\n        noverflow: 0,\n        hash0: 0,\n        buckets: unsafe.Pointer(0x0),\n        oldbuckets: unsafe.Pointer(0x0),\n        nevacuate: 0,\n        extra: *runtime.mapextra nil,}\n```\n\n（1）首先会通过 hint \\* 桶的大小，初步判断**有没有可能出现一次分配的内存 \u003e 预计内存上界**，如果超过，会将 hint 置零\n\n```go\nmem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)\nif overflow || mem \u003e maxAlloc {\n\thint = 0\n}\n```\n\n（2）初始化 hmap 和 哈希种子，设定 **hash0** **随机种子**（每个 map 不同），抵抗哈希碰撞攻击，哈希函数会结合这个种子。\n\n```go\nif h == nil {\n\th = new(hmap)\n}\nh.hash0 = uint32(rand())\n```\n\n（3）决定初始桶指数 B，我们知道 hamp 下面的桶数组是 2^B 大小，也就是说有 2^B 个溢出桶链表。overLoadFactor(hint, B) 是找到不超过 HashMap 负载因子（防止 Hash 劣化）的 B 的最小值。\n\n```go\nB := uint8(0)\nfor overLoadFactor(hint, B) {\n    B++\n}\nh.B = B\n```\n\n（4）分配桶数组，当 B 等于 0 的时候懒分配（即插入的时候再分配），否则直接分配桶，并且可能预留一组溢出桶，以便溢出的时候快速分配。\n\n```go\nif h.B != 0 {\n\tvar nextOverflow *bmap\n\th.buckets, nextOverflow = makeBucketArray(t, h.B, nil)\n\tif nextOverflow != nil {\n\t\th.extra = new(mapextra)\n\t\th.extra.nextOverflow = nextOverflow\n\t}\n}\n```\n\n（5）重点关注下 `makeBucketArray()` 方法，首先 `bucketShift()` 就是计算 2^B，也就是得到桶数组长度。当 b \u003e= 4 的时候会提前分配出一些溢出桶备用。  \n**因为当 b \u003c 4 的时候，基本上不会溢出，根据平均负载阈值是 6.5 ，假设有 K 个桶，每个桶的平均容纳 6.5 个 kv 的时候，就需要增大 B 扩容，而当 B = 1 ～ 3 的时候，桶数量对应 1 ～ 8，扩容阈值是 6 ～ 52。换言之，桶基本上不会达到溢出的地步，map 就会触发增量扩容**\n\n```go\n\nfunc makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {\n\tbase := bucketShift(b)\n\tnbuckets := base\n\t// 这里为什么会有这么一个判断？\n\tif b \u003e= 4 {\n\t\tnbuckets += bucketShift(b - 4)\n\t\tsz := t.Bucket.Size_ * nbuckets\n\t\tup := roundupsize(sz, !t.Bucket.Pointers())\n\t\tif up != sz {\n\t\t\tnbuckets = up / t.Bucket.Size_\n\t\t}\n\t}\n\t// 分配桶数组\n\tif dirtyalloc == nil {\n\t\tbuckets = newarray(t.Bucket, int(nbuckets))\n\t} else {\n\t\tbuckets = dirtyalloc\n\t\tsize := t.Bucket.Size_ * nbuckets\n\t\tif t.Bucket.Pointers() {\n\t\t\tmemclrHasPointers(buckets, size)\n\t\t} else {\n\t\t\tmemclrNoHeapPointers(buckets, size)\n\t\t}\n\t}\n\t// 预分配一些溢出桶\n\tif base != nbuckets {\n\t\tnextOverflow = (*bmap)(add(buckets, base*uintptr(t.BucketSize)))\n\t\tlast := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.BucketSize)))\n\t\tlast.setoverflow(t, (*bmap)(buckets))\n\t}\n\treturn buckets, nextOverflow\n}\n```\n\n### 2.2.2 查询\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030230547252.png)\n\n---\n\n查询 map 实际上走的是 `runtime.mapaccess1()` 或者 `runtime.mapaccess2()`方法。我们不妨也来追踪一下。\n\n```go\nfunc main() {\n\tcache := make(map[string]int, 16)\n\tcache[\"A\"] = 123\n\n\t_ = cache[\"A\"]\n}\n```\n\n如果用直接 key 去获得 v 走的是 `runtime.mapaccess1_faststr()` 方法。这是什么？其实是这样，我们可以在 `src/runtime/` 下发现有这么几个文件 `map_fast32_noswiss.go` 、`map_fast64_noswiss.go` 、`map_faststr_noswiss.go` 以及 `map_noswiss.go` 我们本次讲解主要围绕 `map_noswiss.go` 这个文件展开，其他的实际上是针对不同的 Key 的类型，做了特定的优化，其本质思想是类似的，所以这里不展开。  \n其次，我们还有另外一种方式访问 map 就是 `v, exist := cache[\"A\"]` ，当使用这种方法的时候，会返回 key 是否在 map 中的标记，底层走的是 `mapaccess2()` 方法，其核心与 `mapaccess1()` 类似，这里也不再赘述。\n\n```shell\n❯ dlv debug main.go\nType 'help' for list of commands.\n(dlv) break runtime.mapaccess1_faststr\nBreakpoint 1 set at 0x1045fcf90 for runtime.mapaccess1_faststr() /usr/local/go/src/runtime/map_faststr.go:13\n(dlv) continue\n\u003e [Breakpoint 1] runtime.mapaccess1_faststr() /usr/local/go/src/runtime/map_faststr.go:13 (hits goroutine(1):1 total:1) (PC: 0x1045fcf90)\nWarning: debugging optimized function\n     8:         \"internal/abi\"\n     9:         \"internal/goarch\"\n    10:         \"unsafe\"\n    11: )\n    12:\n=\u003e  13: func mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer {\n    14:         if raceenabled \u0026\u0026 h != nil {\n    15:                 callerpc := getcallerpc()\n    16:                 racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapaccess1_faststr))\n    17:         }\n    18:         if h == nil || h.count == 0 {\n(dlv) args\nt = (\"*internal/abi.MapType\")(0x10466bac0)\nh = (*runtime.hmap)(0x14000052728)\nky = \"A\"\n~r0 = (unreadable empty OP stack)\n```\n\n我们走进具体的方法去观察到底发生了什么？\n\n```go\nfunc mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n\t// ...\n\tif h == nil || h.count == 0 {\n\t\tif err := mapKeyError(t, key); err != nil {\n\t\t\tpanic(err) // see issue 23734\n\t\t}\n\t\treturn unsafe.Pointer(\u0026zeroVal[0])\n\t}\n\tif h.flags\u0026hashWriting != 0 {\n\t\tfatal(\"concurrent map read and map write\")\n\t}\n\thash := t.Hasher(key, uintptr(h.hash0))\n\tm := bucketMask(h.B)\n\tb := (*bmap)(add(h.buckets, (hash\u0026m)*uintptr(t.BucketSize)))\n\tif c := h.oldbuckets; c != nil {\n\t\tif !h.sameSizeGrow() {\n\t\t\tm \u003e\u003e= 1\n\t\t}\n\t\toldb := (*bmap)(add(c, (hash\u0026m)*uintptr(t.BucketSize)))\n\t\tif !evacuated(oldb) {\n\t\t\tb = oldb\n\t\t}\n\t}\n\ttop := tophash(hash)\nbucketloop:\n\tfor ; b != nil; b = b.overflow(t) {\n\t\tfor i := uintptr(0); i \u003c abi.OldMapBucketCount; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tif b.tophash[i] == emptyRest {\n\t\t\t\t\tbreak bucketloop\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\t\tif t.IndirectKey() {\n\t\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t\t}\n\t\t\tif t.Key.Equal(key, k) {\n\t\t\t\te := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\t\tif t.IndirectElem() {\n\t\t\t\t\te = *((*unsafe.Pointer)(e))\n\t\t\t\t}\n\t\t\t\treturn e\n\t\t\t}\n\t\t}\n\t}\n\treturn unsafe.Pointer(\u0026zeroVal[0])\n}\n```\n\n（1）当 map 为 nil 或者无元素时，返回对应 val 的零值\n\n```go\nif h == nil || h.count == 0 {\n\tif err := mapKeyError(t, key); err != nil {\n\t\tpanic(err) // see issue 23734\n\t}\n\treturn unsafe.Pointer(\u0026zeroVal[0])\n}\n```\n\n（2）判断当前 map 是否正在被写入，如果发现有其他协程在写入 map，则直接 fatal。\n\n```go\n// 是否正在写入\ncosnt hashWriting  = 4 // a goroutine is writing to the map\n// 状态按位 \u0026 来判断是否在写入，利用位运算是底层代码常见的方案\nif h.flags\u0026hashWriting != 0 {\n\tfatal(\"concurrent map read and map write\")\n}\n```\n\n（3）计算哈希、定位桶。通过 `t.Hasher()` 计算出 key 对应的 hash 值，并且计算出桶数组长度 - 1（用于计算 Hash 低 B 位的掩码），进而计算出数组中桶的位置。\n\n```go\nhash := t.Hasher(key, uintptr(h.hash0))\nm := bucketMask(h.B)\n// add 是地址 + 偏移量\n// (hash\u0026m) ==\u003e hash % (1 \u003c\u003c B)\nb := (*bmap)(add(h.buckets, (hash\u0026m)*uintptr(t.BucketSize)))\n```\n\n（4）扩容时 oldbuckets 指向旧表；未搬迁的桶优先从旧桶查。增量扩容时，旧表桶数是新表的一半，所以再右移一位 m，这样才能得到增量扩容前旧桶中对应的桶址。如果旧桶此时还没搬迁完，则将寻找目标改为旧桶\n\n```go\nif c := h.oldbuckets; c != nil {\n\tif !h.sameSizeGrow() {m \u003e\u003e= 1}\n\toldb := (*bmap)(add(c, (hash\u0026m)*uintptr(t.BucketSize)))\n\tif !evacuated(oldb) {\n\t\t// 转到旧桶搜索\n\t\tb = oldb\n\t}\n}\n// 获取 hash 值的高 8 位，如果高 8 位算下小于 5，则会 + 5，来避开下面提到的特殊标记。\ntop := tophash(hash)\n```\n\nb.tophash[i] 平时存放键哈希的高 8 位。但 **小于 minTopHash 的值都不是正常键**，而是状态标记。**（当 h ∈ {2, 3, 4} 的时候，表示该桶以及完成了搬迁）**\n\n```go\nconst empty = 0    // 该槽为空\nconst emptyOne = 1    // 空，且其后全为空（搜索可早停）\nconst evacuatedX = 2    // 桶已搬到低位桶（当增量扩容时，新表长度是旧表两倍，所以会被拆分成两个桶）\nconst evacuatedY = 3    // 桶已搬到高位桶\nconst evacuatedEmpty = 4     // 已搬迁且为空\nconst minTopHash = 5    // 正常键的 tophash 值从 5 起\n\nfunc evacuated(b *bmap) bool {\n    h := b.tophash[0]\n    return h \u003e emptyOne \u0026\u0026 h \u003c minTopHash\n}\n\nfunc tophash(hash uintptr) uint8 {\n\ttop := uint8(hash \u003e\u003e (goarch.PtrSize*8 - 8))\n\tif top \u003c minTopHash {\n\t\t// 避开标记位\n\t\ttop += minTopHash\n\t}\n\treturn top\n}\n```\n\n（5）遍历启动！先看外层循环，外层就是沿着溢出桶链表，遍历每个溢出桶。内层逻辑是首先一个桶最多放 8 个 kv 对，接着先用高 8 位 hash 判断，只有高 8 位 hash 相等再判断整个 key，进一步提升效率，不然每次都需要对比完整的 key。通过 KeySize 计算得到 k。接着校验 k 和查找的 key 是否相同，相同则返回 val。如果找遍了 overflowBmap 都没有找到 key，则直接返回 value 零值。\n\n```go\nbucketloop:\n\tfor ; b != nil; b = b.overflow(t) {\n\t\t// abi.OldMapBucketCount = 8\n\t\tfor i := uintptr(0); i \u003c abi.OldMapBucketCount; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tif b.tophash[i] == emptyRest {\n\t\t\t\t\tbreak bucketloop\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\t\tif t.IndirectKey() {\n\t\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t\t}\n\t\t\t// 检查 key 是否相等，相等直接返回\n\t\t\tif t.Key.Equal(key, k) {\n\t\t\t\te := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\t\tif t.IndirectElem() {\n\t\t\t\t\te = *((*unsafe.Pointer)(e))\n\t\t\t\t}\n\t\t\t\treturn e\n\t\t\t}\n\t\t}\n\t}\n\treturn unsafe.Pointer(\u0026zeroVal[0])\n```\n\n### 2.2.3 插入\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030234027743.png)\n\n---\n\n接着就是插入元素啦，让我们来康康走的是那个方法？\n```go\nfunc main() {\n\t// 这里用 interface{} 是因为一些基础类型做 key 的时候，都会走优化过的方法\n\tcache := make(map[interface{}]int, 16)\n\tcache[1] = 123\n\n\tprintln(cache[1])\n}\n```\n\n可以看出，代码最后进入了 `runtime.mapassign()` 方法，这也正是 map 对应的插入相关的方法。\n这里与读同理，对于一些基础类型 int、string 做 key 的 map，其底层有其他对应的优化方法，但本质类似，所以这里我们直接看本源方法 `runtime.mapassign()` 。\n\n```shell\n❯ dlv debug main.go\nType 'help' for list of commands.\n(dlv) break runtime.mapassign\nBreakpoint 1 set at 0x102f3997c for runtime.mapassign() /usr/local/go/src/runtime/map.go:615\n(dlv) continue\n\u003e [Breakpoint 1] runtime.mapassign() /usr/local/go/src/runtime/map.go:615 (hits goroutine(1):1 total:1) (PC: 0x102f3997c)\nWarning: debugging optimized function\n   610: //\n   611: // Do not remove or change the type signature.\n   612: // See go.dev/issue/67401.\n   613: //\n   614: //go:linkname mapassign\n=\u003e 615: func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n   616:         if h == nil {\n   617:                 panic(plainError(\"assignment to entry in nil map\"))\n   618:         }\n   619:         if raceenabled {\n   620:                 callerpc := getcallerpc()\n(dlv) args\nt = (\"*internal/abi.MapType\")(0x102f57ba0)\nh = (*runtime.hmap)(0x14000052718)\nkey = unsafe.Pointer(0x14000052748)\n~r0 = (unreadable empty OP stack)\n```\n\n让我们一探之究竟，首先一个疑问，插入为什么只传入了插入的 key，而没有 value？  \n**插入操作的方法不只是 mapassign()，实际上 mapassign() 只负责返回一个插入的值的地址，接着由其他方法插入，所以 mapassign() 之所以叫 assign 而不叫 update 或者 insert**\n\n```go\n//go:linkname mapassign\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n\t// ... 工具检查\n\tif h.flags\u0026hashWriting != 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\thash := t.Hasher(key, uintptr(h.hash0))\n\n\th.flags ^= hashWriting\n\n\tif h.buckets == nil {\n\t\th.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)\n\t}\n\nagain:\n\tbucket := hash \u0026 bucketMask(h.B)\n\tif h.growing() {\n\t\tgrowWork(t, h, bucket)\n\t}\n\tb := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))\n\ttop := tophash(hash)\n\n\tvar inserti *uint8\n\tvar insertk unsafe.Pointer\n\tvar elem unsafe.Pointer\nbucketloop:\n\tfor {\n\t\tfor i := uintptr(0); i \u003c abi.OldMapBucketCount; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tif isEmpty(b.tophash[i]) \u0026\u0026 inserti == nil {\n\t\t\t\t\tinserti = \u0026b.tophash[i]\n\t\t\t\t\tinsertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\t\t\t\telem = add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\t\t}\n\t\t\t\tif b.tophash[i] == emptyRest {\n\t\t\t\t\tbreak bucketloop\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\t\tif t.IndirectKey() {\n\t\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t\t}\n\t\t\tif !t.Key.Equal(key, k) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// already have a mapping for key. Update it.\n\t\t\tif t.NeedKeyUpdate() {\n\t\t\t\ttypedmemmove(t.Key, k, key)\n\t\t\t}\n\t\t\telem = add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\tgoto done\n\t\t}\n\t\tovf := b.overflow(t)\n\t\tif ovf == nil {\n\t\t\tbreak\n\t\t}\n\t\tb = ovf\n\t}\n\n\tif !h.growing() \u0026\u0026 (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\t\thashGrow(t, h)\n\t\tgoto again // Growing the table invalidates everything, so try again\n\t}\n\n\tif inserti == nil {\n\t\tnewb := h.newoverflow(t, b)\n\t\tinserti = \u0026newb.tophash[0]\n\t\tinsertk = add(unsafe.Pointer(newb), dataOffset)\n\t\telem = add(insertk, abi.OldMapBucketCount*uintptr(t.KeySize))\n\t}\n\n\tif t.IndirectKey() {\n\t\tkmem := newobject(t.Key)\n\t\t*(*unsafe.Pointer)(insertk) = kmem\n\t\tinsertk = kmem\n\t}\n\tif t.IndirectElem() {\n\t\tvmem := newobject(t.Elem)\n\t\t*(*unsafe.Pointer)(elem) = vmem\n\t}\n\ttypedmemmove(t.Key, insertk, key)\n\t*inserti = top\n\th.count++\n\ndone:\n\tif h.flags\u0026hashWriting == 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\th.flags \u0026^= hashWriting\n\tif t.IndirectElem() {\n\t\telem = *((*unsafe.Pointer)(elem))\n\t}\n\treturn elem\n}\n```\n\n（1）并发写保护并且设置标记位，懒分配 buckets。这里就是一个简单的并发写保护，并且计算出 key 的 hash 值，然后将 hmap 的 flag 设置为正在写的状态（**这里要注意的时候，写状态要在成功计算出 hash 后在修改，因为 t.Hasher() 也有可能 panic**）。其次，**我们在开头分配 bucket 的时候，当 B = 0 时，采用的是懒分配，也就是插入的时候分配，而分配的时间就是现在**。\n\n```go\nif h.flags\u0026hashWriting != 0 {\n\tfatal(\"concurrent map writes\")\n}\nhash := t.Hasher(key, uintptr(h.hash0))\n\nh.flags ^= hashWriting\n\nif h.buckets == nil {\n\th.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)\n}\n```\n\n（2） 定位目标桶，如果当前 map 处在扩容的状态，则帮助命中的桶进行扩容。因为这里涉及到了扩容的部分，内容较多，详情可以看 2.2.5 小节\n\n```go\nbucket := hash \u0026 bucketMask(h.B)\nif h.growing() { growWork(t, h, bucket) }\nb := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))\ntop := tophash(hash)\n```\n\n（3）遍历记录 tophash 首次可写的位置，以及这个位置对应的 key 和 val 的地址。\n\n```go\nvar inserti *uint8\nvar insertk unsafe.Pointer\nvar elem   unsafe.Pointer\nbucketloop:\nfor {\n\tfor i := 0; i \u003c 8; i++ {\n\t\t// 先用 tophash 过滤\n\t    if b.tophash[i] != top {\n\t\t    if isEmpty(b.tophash[i]) \u0026\u0026 inserti == nil {\n\t\t        // 首次见到 empty/evacuated 槽，先占住这个槽，后续如果查到相同 key 则更新\n\t\t        inserti = \u0026b.tophash[i]\n\t\t        insertk = keySlotAddr(b, i)\n\t\t        elem    = valSlotAddr(b, i)\n\t\t    }\n\t\t    // 后面全空，整条链早停\n\t\t    if b.tophash[i] == emptyRest {\n\t\t        break bucketloop\n\t\t    }\n\t\t\tcontinue\n\t    }\n\t    k := keySlotAddr(b, i)\n\t    if t.IndirectKey() { k = *(*unsafe.Pointer)(k) }\n\t    // 这里是判断是不是以及存在这个 key，如果存在这直接更新对应的 kv\n\t    if t.Key.Equal(key, k) {\n\t\t    if t.NeedKeyUpdate() { typedmemmove(t.Key, k, key) }\n\t\t    elem = valSlotAddr(b, i)\n\t\t    goto done\n\t    }\n\t}\n\tif ovf := b.overflow(t); ovf != nil { b = ovf; continue }\n\tbreak\n}\n```\n\n（4）在上一步中，如果没能找到一样的 key，则代表这是一次插入操作，插入操作就有可能引发 Hash 劣化，此时就需要先判断是否需要扩容了。这里因为涉及到扩容，所以也在 2.2.5 小节展开。\n\n```go\nif !h.growing() \u0026\u0026 (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\thashGrow(t, h)\n\tgoto again // Growing the table invalidates everything, so try again\n}\n```\n\n（5）在溢出桶中没有找到空位，就挂 overflow 溢出，这一步就是创建新的溢出桶（**这里会优先使用一开始创建 map 的时候冗余创建的溢出桶，当冗余桶不够的时候，则会创建新的溢出桶**）\n\n```go\nif inserti == nil {\n\tnewb := h.newoverflow(t, b)\n\tinserti = \u0026newb.tophash[0]\n\tinsertk = add(unsafe.Pointer(newb), dataOffset)\n\telem = add(insertk, abi.OldMapBucketCount*uintptr(t.KeySize))\n}\n```\n\n（6）为 kv 预插入操作（**注意这里不是直接插入，尤其是 value**），首先会先按照 key 和 val 分配对象，然后先把这个空对象塞到槽内，然后使用 `typedmemmove(t.Key, insertk, key)` 将调用方传入的 Key 拷贝到最终位置，而 value 是由上层的调用方写入的。\n\n```go\n// store new key/elem at insert position\nif t.IndirectKey() {\n\tkmem := newobject(t.Key)\n\t*(*unsafe.Pointer)(insertk) = kmem\n\tinsertk = kmem\n}\nif t.IndirectElem() {\n\tvmem := newobject(t.Elem)\n\t*(*unsafe.Pointer)(elem) = vmem\n}\ntypedmemmove(t.Key, insertk, key)\n*inserti = top\nh.count++\n```\n\n（7）收尾向上层返回最终确定要插入的 value 的地址\n\n```go\ndone:\n\tif h.flags\u0026hashWriting == 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\th.flags \u0026^= hashWriting\n\tif t.IndirectElem() {\n\t\telem = *((*unsafe.Pointer)(elem))\n\t}\n\treturn elem\n```\n\n### 2.2.4 删除\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030234416131.png)\n\n---\n\n通过类似的 debug 手段，不难看出删除实际上是调用了 `runtime.mapdelete()` 方法\n\n```go\n//go:linkname mapdelete\nfunc mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {\n\t// ... 工具配置\n\tif h == nil || h.count == 0 {\n\t\tif err := mapKeyError(t, key); err != nil {\n\t\t\tpanic(err) // see issue 23734\n\t\t}\n\t\treturn\n\t}\n\tif h.flags\u0026hashWriting != 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\n\thash := t.Hasher(key, uintptr(h.hash0))\n\n\th.flags ^= hashWriting\n\n\tbucket := hash \u0026 bucketMask(h.B)\n\tif h.growing() {\n\t\tgrowWork(t, h, bucket)\n\t}\n\tb := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))\n\tbOrig := b\n\ttop := tophash(hash)\nsearch:\n\tfor ; b != nil; b = b.overflow(t) {\n\t\tfor i := uintptr(0); i \u003c abi.OldMapBucketCount; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tif b.tophash[i] == emptyRest {\n\t\t\t\t\tbreak search\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\t\tk2 := k\n\t\t\tif t.IndirectKey() {\n\t\t\t\tk2 = *((*unsafe.Pointer)(k2))\n\t\t\t}\n\t\t\tif !t.Key.Equal(key, k2) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Only clear key if there are pointers in it.\n\t\t\tif t.IndirectKey() {\n\t\t\t\t*(*unsafe.Pointer)(k) = nil\n\t\t\t} else if t.Key.Pointers() {\n\t\t\t\tmemclrHasPointers(k, t.Key.Size_)\n\t\t\t}\n\t\t\te := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\tif t.IndirectElem() {\n\t\t\t\t*(*unsafe.Pointer)(e) = nil\n\t\t\t} else if t.Elem.Pointers() {\n\t\t\t\tmemclrHasPointers(e, t.Elem.Size_)\n\t\t\t} else {\n\t\t\t\tmemclrNoHeapPointers(e, t.Elem.Size_)\n\t\t\t}\n\t\t\tb.tophash[i] = emptyOne\n\t\t\tif i == abi.OldMapBucketCount-1 {\n\t\t\t\tif b.overflow(t) != nil \u0026\u0026 b.overflow(t).tophash[0] != emptyRest {\n\t\t\t\t\tgoto notLast\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif b.tophash[i+1] != emptyRest {\n\t\t\t\t\tgoto notLast\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor {\n\t\t\t\tb.tophash[i] = emptyRest\n\t\t\t\tif i == 0 {\n\t\t\t\t\tif b == bOrig {\n\t\t\t\t\t\tbreak // beginning of initial bucket, we're done.\n\t\t\t\t\t}\n\t\t\t\t\t// Find previous bucket, continue at its last entry.\n\t\t\t\t\tc := b\n\t\t\t\t\tfor b = bOrig; b.overflow(t) != c; b = b.overflow(t) {\n\t\t\t\t\t}\n\t\t\t\t\ti = abi.OldMapBucketCount - 1\n\t\t\t\t} else {\n\t\t\t\t\ti--\n\t\t\t\t}\n\t\t\t\tif b.tophash[i] != emptyOne {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\tnotLast:\n\t\t\th.count--\n\t\t\tif h.count == 0 {\n\t\t\t\th.hash0 = uint32(rand())\n\t\t\t}\n\t\t\tbreak search\n\t\t}\n\t}\n\n\tif h.flags\u0026hashWriting == 0 {\n\t\tfatal(\"concurrent map writes\")\n\t}\n\th.flags \u0026^= hashWriting\n}\n```\n\n（1）安全性校验以及设置标记位\n\n```go\nif h == nil || h.count == 0 {\n\tif err := mapKeyError(t, key); err != nil {\n\t\tpanic(err) // see issue 23734\n\t}\n\treturn\n}\nif h.flags\u0026hashWriting != 0 {\n\tfatal(\"concurrent map writes\")\n}\n\nhash := t.Hasher(key, uintptr(h.hash0))\n// 这里类似插入，标记位设置要在 Hasher 之后，因为 Hasher 方法可能 panic\nh.flags ^= hashWriting\n```\n\n（2）定位目标桶，如果当前 map 正处于扩容状态，则帮助桶进行扩容，接着算出当前要查找的桶是哪个，然后算出高 8 位 hash 值用来做快速对比。\n\n```go\nbucket := hash \u0026 bucketMask(h.B)\nif h.growing() {\n\tgrowWork(t, h, bucket)\n}\nb := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))\nbOrig := b\ntop := tophash(hash)\n```\n\n（3）接着是和前面很类似的查找定位 key 的操作，一样的先通过 tophash 快筛，然后再对比 key\n\n```go\nfor ; b != nil; b = b.overflow(t) {\n\tfor i := uintptr(0); i \u003c abi.OldMapBucketCount; i++ {\n\t\tif b.tophash[i] != top {\n\t\t\tif b.tophash[i] == emptyRest {\n\t\t\t\tbreak search\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))\n\t\tk2 := k\n\t\tif t.IndirectKey() {\n\t\t\tk2 = *((*unsafe.Pointer)(k2))\n\t\t}\n\t\tif !t.Key.Equal(key, k2) {\n\t\t\tcontinue\n\t\t}\n\t// ...\n```\n\n（4）当找到了对应的 key，则清理对应 bmap 的 tophash，以及之后的 kv 数据，这里就是计位置，然后删除 kv，然后将对应的 tophash 槽置为空。\n\n```go\n// Only clear key if there are pointers in it.\n\t\t\tif t.IndirectKey() {\n\t\t\t\t*(*unsafe.Pointer)(k) = nil\n\t\t\t} else if t.Key.Pointers() {\n\t\t\t\tmemclrHasPointers(k, t.Key.Size_)\n\t\t\t}\n\t\t\te := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))\n\t\t\tif t.IndirectElem() {\n\t\t\t\t*(*unsafe.Pointer)(e) = nil\n\t\t\t} else if t.Elem.Pointers() {\n\t\t\t\tmemclrHasPointers(e, t.Elem.Size_)\n\t\t\t} else {\n\t\t\t\tmemclrNoHeapPointers(e, t.Elem.Size_)\n\t\t\t}\n\t\t\tb.tophash[i] = emptyOne\n```\n\n（5）早停设计，记不记得我们之前在讲 tophash 的时候他有几个独特的标记位，其中有一个是早停的标记位，也就是当搜索的时候，如果发现当前` tophash == emptyRest` ，代表这个位置之后都没有 key 了，可以早停。**当我们删除 key 后，需要判断一下当前位能不能设置为早停位，如果可以，就可以往前寻找找到最早的可以设置为早停的地方。**\n\n```go\nif i == abi.OldMapBucketCount-1 {\n\tif b.overflow(t) != nil \u0026\u0026 b.overflow(t).tophash[0] != emptyRest {\n\t\tgoto notLast\n\t}\n} else {\n\tif b.tophash[i+1] != emptyRest {\n\t\tgoto notLast\n\t}\n}\n// 代码能执行到这里就说明这个 key 后的所有槽都是空的\n// 此时会将这个槽的 tophash 设置为早停标记\nfor {\n\tb.tophash[i] = emptyRest\n\tif i == 0 {\n\t\tif b == bOrig {\n\t\t\tbreak // beginning of initial bucket, we're done.\n\t\t}\n\t// 如果当前 key 是桶内第一个 key，则找到当前桶的前一个桶\n\t\tc := b\n\t\tfor b = bOrig; b.overflow(t) != c; b = b.overflow(t) {}\n\t\ti = abi.OldMapBucketCount - 1\n\t} else {\n\t\ti--\n\t}\n\t// 如果槽不为空，则直接退出\n\tif b.tophash[i] != emptyOne {\n\t\tbreak\n\t}\n}\n```\n\n（6）计数器减一，如果此时没有 kv 在 map 中，重新设置 hash 种子\n\n```go\nnotLast:\n\th.count--\n\tif h.count == 0 {\n\t\th.hash0 = uint32(rand())\n\t}\n\tbreak search\n```\n\n（7）退出，安全性检查以及标记位恢复\n\n```go\n// 校验是否有并发写\nif h.flags\u0026hashWriting == 0 {\n\tfatal(\"concurrent map writes\")\n}\nh.flags \u0026^= hashWriting\n```\n\n### 2.2.5 扩容\n\n#### 增量扩容\n\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030235441064.png)\n如上图，当存储密度很高且链足够长的时候，此时会触发增量扩容，也就是扩大 Hash 寻址的范围。数值上来看就是 Buket 数组翻倍，B + 1。进而把同一个溢出桶链上的 kv 打散。如下图\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251030235932898.png)\n\n#### 等量扩容\n\n我们的 map 会经常增删，但是删除的时候，并不会对桶进行回收，所以有可能导致链很长，但是存储密度很低，进而导致 hash 劣化。如下图这种极端情况，一个 buckt 里虽然只有两个 kv 对，但是要搜索完好几个桶才能定位。\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251031000400469.png)\n此时会触发等量扩容，桶数组长度不变，转而压缩桶内数据，从而提高存储密度。如下图\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251031000433992.png)\n\n---\n\n通过上面的增删改查的代码，可以发现只有 map 插入元素的时候，可能会触发扩容。对应的代码片段也就是 `runtime.mapassign()` 部分。\n当当前 map 不在扩容过程中，且满足下面两个条件之一的时候，则会开启扩容：\n\n1. `overLoadFactor(h.count+1, h.B)` 判断如果再插入一个元素，会不会导致存储密度超出阈值，进而导致 hash 劣化\n2. `tooManyOverflowBuckets(h.noverflow, h.B)` 判断是否是存储密度太小，但是溢出桶太多导致的 hash 劣化\n   上面两点也就对应了我们上面所说到的两种 map 的扩容方式。\n\n```go\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n\t// ...\n\t// Did not find mapping for key. Allocate new cell \u0026 add entry.\n\t// If we hit the max load factor or we have too many overflow buckets,\n\t// and we're not already in the middle of growing, start growing.\n\t// 扩容\n\tif !h.growing() \u0026\u0026 (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\t\thashGrow(t, h)\n\t\tgoto again // Growing the table invalidates everything, so try again\n\t}\n\t// ...\n}\n```\n\n触发扩容后，会进入 `hashGrow()` 方法，这里会执行 map 扩容的流程。\n\n```go\nfunc hashGrow(t *maptype, h *hmap) {\n\tbigger := uint8(1)\n\tif !overLoadFactor(h.count+1, h.B) {\n\t\tbigger = 0\n\t\th.flags |= sameSizeGrow\n\t}\n\toldbuckets := h.buckets\n\tnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)\n\n\tflags := h.flags \u0026^ (iterator | oldIterator)\n\tif h.flags\u0026iterator != 0 {\n\t\tflags |= oldIterator\n\t}\n\t// commit the grow (atomic wrt gc)\n\th.B += bigger\n\th.flags = flags\n\th.oldbuckets = oldbuckets\n\th.buckets = newbuckets\n\th.nevacuate = 0\n\th.noverflow = 0\n\n\tif h.extra != nil \u0026\u0026 h.extra.overflow != nil {\n\t\t// Promote current overflow buckets to the old generation.\n\t\tif h.extra.oldoverflow != nil {\n\t\t\tthrow(\"oldoverflow is not nil\")\n\t\t}\n\t\th.extra.oldoverflow = h.extra.overflow\n\t\th.extra.overflow = nil\n\t}\n\tif nextOverflow != nil {\n\t\tif h.extra == nil {\n\t\t\th.extra = new(mapextra)\n\t\t}\n\t\th.extra.nextOverflow = nextOverflow\n\t}\n}\n```\n\n（1）首先，无论是等量扩容还是增量扩容，都会创建新的 bucket 数组，只不过等量不会扩大这个数组的长度而已。这里判断一下要分配的桶数组长度，增量扩容的话，数组长度翻倍。\n\n```go\nif !overLoadFactor(h.count+1, h.B) {\n\tbigger = 0\n\th.flags |= sameSizeGrow\n}\noldbuckets := h.buckets\nnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)\n```\n\n（2）设置标记位，修改 map 头中的元数据，更新 B、flag，将头中的新老桶指针分别指向旧桶和新桶数组，将扩容进度 nevacuate 置为 0，新桶的溢出桶数量置为 0。\n\n```go\nflags := h.flags \u0026^ (iterator | oldIterator)\nif h.flags\u0026iterator != 0 {\n\tflags |= oldIterator\n}\n// commit the grow (atomic wrt gc)\nh.B += bigger\nh.flags = flags\nh.oldbuckets = oldbuckets\nh.buckets = newbuckets\nh.nevacuate = 0\nh.noverflow = 0\n```\n\n（3）将 map 头中的 extra 的旧溢出桶数组字段 `hmap.extra.oldoverflow` 指向溢出桶字段，因为渐进式扩容，需要一步一步搬溢出桶中的数据，然后溢出桶数组改为 `hmap.extra.overflow` 置为空，用来存储新的 bucket 的溢出桶地址（**前面提到过，不只有 bmap 中虽然会逻辑层面划分后面的部分给 kv 数组以及 nextOverflow 指针，但是为了防止 GC 误回收那片区域，所以这里 extra 中的字段会额外存储对应的 nextOverflow 指针，这里实际上也有点意思，感兴趣的读者可以了解一下为什么要单独用 h.extra 存放所有的溢出桶，而 k 和 v 不需要**）。最后就是将一开始创建好待用的溢出桶确保其存在就行。\n\n```go\nif h.extra != nil \u0026\u0026 h.extra.overflow != nil {\n\t// Promote current overflow buckets to the old generation.\n\tif h.extra.oldoverflow != nil {\n\t\tthrow(\"oldoverflow is not nil\")\n\t}\n\th.extra.oldoverflow = h.extra.overflow\n\th.extra.overflow = nil\n}\nif nextOverflow != nil {\n\tif h.extra == nil {\n\t\th.extra = new(mapextra)\n\t}\n\th.extra.nextOverflow = nextOverflow\n}\n```\n\n至此，扩容就完毕了，接下来就是迁移。从之前的插入源码可以得到，迁移部分的代码入口是`runtime.growWord()` 方法，这里会先帮助本次插入涉及到的桶完成迁移，然后再额外辅助迁移一个桶。\n\n```go\nfunc growWork(t *maptype, h *hmap, bucket uintptr) {\n\t// 确保本次命中的桶完成迁移\n\tevacuate(t, h, bucket\u0026h.oldbucketmask())\n\n\t// 多帮忙搬迁一个桶推进增量扩容的进度\n\tif h.growing() {\n\t\tevacuate(t, h, h.nevacuate)\n\t}\n}\n```\n\n这里我们也看到了，实际上搬迁的主要方法是 `exacuate()` 方法，进一步剖析该方法（注释形式）\n\n```go\nfunc evacuate(t *maptype, h *hmap, oldbucket uintptr) {\n\t// 获取到桶的位置\n\tb := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))\n\tnewbit := h.noldbuckets()\n\t// 检查旧桶是否迁移完毕，当未完成迁移，则迁移\n\tif !evacuated(b) {\n\t\t// 这里会计算两个 rehash 的目标位置，第二个位置会根据扩容的类型来选择\n\t\t// 我们知道等量扩容时其 hash 地址是不会变的，这些地址不变的 kv 这里就对应了地址 x\n\t\t// 当增量扩容模式下，有的 kv 会被 rehash 到其他溢出桶，这里对应的是下面的 y\n\t\tvar xy [2]evacDst\n\t\tx := \u0026xy[0]\n\t\tx.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.BucketSize)))\n\t\tx.k = add(unsafe.Pointer(x.b), dataOffset)\n\t\tx.e = add(x.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n\n\t\tif !h.sameSizeGrow() {\n\t\t\ty := \u0026xy[1]\n\t\t\ty.b = (*bmap)(add(h.buckets,(oldbucket+newbit)*uintptr(t.BucketSize)))\n\t\t\ty.k = add(unsafe.Pointer(y.b), dataOffset)\n\t\t\ty.e = add(y.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n\t\t}\n\t\t// 这里的遍历思路也是外层沿着溢出桶指针，内层遍历溢出桶内的 8 个 kv\n\t\tfor ; b != nil; b = b.overflow(t) {\n\t\t\t// 当前桶 keys[0] 起始地址\n\t\t\tk := add(unsafe.Pointer(b), dataOffset)\n\t\t\t// 当前桶 values[0] 起始地址\n\t\t\te := add(k, abi.OldMapBucketCount*uintptr(t.KeySize))\n\t\t\tfor i := 0; i \u003c abi.OldMapBucketCount; i, k, e = i+1, add(k, uintptr(t.KeySize)), add(e, uintptr(t.ValueSize)) {\n\t\t\t\ttop := b.tophash[i]\n\t\t\t\t// 高八位快速判断当前槽内有没有 kv\n\t\t\t\tif isEmpty(top) {\n\t\t\t\t\tb.tophash[i] = evacuatedEmpty\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif top \u003c minTopHash {\n\t\t\t\t\tthrow(\"bad map state\")\n\t\t\t\t}\n\t\t\t\t// 无异常情况，获取到需要搬迁的 key\n\t\t\t\tk2 := k\n\t\t\t\tif t.IndirectKey() {\n\t\t\t\t\tk2 = *((*unsafe.Pointer)(k2))\n\t\t\t\t}\n\t\t\t\t// 这里是判断这个 key 的目的地，只有增量扩容的时候才会用到 y\n\t\t\t\tvar useY uint8\n\t\t\t\tif !h.sameSizeGrow() {\n\t\t\t\t\t// 如果是增量扩容，先计算 hash 值\n\t\t\t\t\thash := t.Hasher(k2, uintptr(h.hash0))\n\n\t\t\t\t\tif h.flags\u0026iterator != 0 \u0026\u0026 !t.ReflexiveKey() \u0026\u0026 !t.Key.Equal(k2, k2) {\n\t\t\t\t\t\tuseY = top \u0026 1\n\t\t\t\t\t\ttop = tophash(hash)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif hash\u0026newbit != 0 {\n\t\t\t\t\t\t\tuseY = 1\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {\n\t\t\t\t\tthrow(\"bad evacuatedN\")\n\t\t\t\t}\n\n\t\t\t\tb.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY\n\t\t\t\tdst := \u0026xy[useY]                 // evacuation destination\n\n\t\t\t\tif dst.i == abi.OldMapBucketCount {\n\t\t\t\t\tdst.b = h.newoverflow(t, dst.b)\n\t\t\t\t\tdst.i = 0\n\t\t\t\t\tdst.k = add(unsafe.Pointer(dst.b), dataOffset)\n\t\t\t\t\tdst.e = add(dst.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n\t\t\t\t}\n\t\t\t\tdst.b.tophash[dst.i\u0026(abi.OldMapBucketCount-1)] = top\n\t\t\t\tif t.IndirectKey() {\n\t\t\t\t\t*(*unsafe.Pointer)(dst.k) = k2 // copy pointer\n\t\t\t\t} else {\n\t\t\t\t\ttypedmemmove(t.Key, dst.k, k) // copy elem\n\t\t\t\t}\n\t\t\t\tif t.IndirectElem() {\n\t\t\t\t\t*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)\n\t\t\t\t} else {\n\t\t\t\t\ttypedmemmove(t.Elem, dst.e, e)\n\t\t\t\t}\n\t\t\t\tdst.i++\n\t\t\t\tdst.k = add(dst.k, uintptr(t.KeySize))\n\t\t\t\tdst.e = add(dst.e, uintptr(t.ValueSize))\n\t\t\t}\n\t\t}\n\n\t\tif h.flags\u0026oldIterator == 0 \u0026\u0026 t.Bucket.Pointers() {\n\t\t\tb := add(h.oldbuckets, oldbucket*uintptr(t.BucketSize))\n\t\t\tptr := add(b, dataOffset)\n\t\t\tn := uintptr(t.BucketSize) - dataOffset\n\t\t\tmemclrHasPointers(ptr, n)\n\t\t}\n\t}\n\n\tif oldbucket == h.nevacuate {\n\t\tadvanceEvacuationMark(h, t, newbit)\n\t}\n}\n```\n\n（1）定位迁移的目的地，这里需要通过判断是否是增量扩容来进一步判断是否需要另一个地址（**因为增量扩容有的 key 会被 rehash 到新桶数组的不同位置**）\n\n```go\nb := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))\nnewbit := h.noldbuckets() // = 1 \u003c\u003c (B_old)\nif !evacuated(b) {\n    var xy [2]evacDst\n    x := \u0026xy[0]\n    x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.BucketSize))) // 低位桶 X（new[i]）\n    x.k = add(unsafe.Pointer(x.b), dataOffset)\n    x.e = add(x.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n\n    if !h.sameSizeGrow() { // 只有“增量扩容”才有 Y 桶\n        y := \u0026xy[1]\n        y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.BucketSize))) // 高位桶 Y（new[i+newbit]）\n        y.k = add(unsafe.Pointer(y.b), dataOffset)\n        y.e = add(y.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n    }\n    ...\n}\n```\n\n（2）首先沿着 overflow 链扫描 kv，然后用 key 的 hash 值的地位是 0/1 判断其在新桶中是原位置 x 还是新位置 y，接着将 kv 以及 tophash 写入目的地。\n\n```go\nfor ; b != nil; b = b.overflow(t) {\n    k := add(unsafe.Pointer(b), dataOffset)\n    e := add(k, abi.OldMapBucketCount*uintptr(t.KeySize))\n    for i := 0; i \u003c abi.OldMapBucketCount; i, k, e = i+1, add(k, KSize), add(e, VSize) {\n        top := b.tophash[i]\n        if isEmpty(top) { b.tophash[i] = evacuatedEmpty; continue }\n        if top \u003c minTopHash { throw(\"bad map state\") }\n\n        // 取 key 地址（考虑间接存放）\n        k2 := k\n        if t.IndirectKey() { k2 = *(*unsafe.Pointer)(k2) }\n\n        // 选 X 还是 Y（只有增量扩容才分流）\n        var useY uint8\n        if !h.sameSizeGrow() {\n            hash := t.Hasher(k2, uintptr(h.hash0))\n            if h.flags\u0026iterator != 0 \u0026\u0026 !t.ReflexiveKey() \u0026\u0026 !t.Key.Equal(k2,k2) {\n                // 非自反键（NaN!=NaN），用旧 tophash 低位稳定分流\n                useY = top \u0026 1\n                top = tophash(hash) // 同时重算新 tophash\n            } else {\n                if hash \u0026 newbit != 0 { useY = 1 } // 高位为 1 则去 Y\n            }\n        }\n\n        // 标记该槽已搬迁到 X/Y\n        b.tophash[i] = evacuatedX + useY // evacuatedX+1 == evacuatedY\n        dst := \u0026xy[useY]\n\n        // 目标桶位已满则挂溢出并重置写指针\n        if dst.i == abi.OldMapBucketCount {\n            dst.b = h.newoverflow(t, dst.b)\n            dst.i = 0\n            dst.k = add(unsafe.Pointer(dst.b), dataOffset)\n            dst.e = add(dst.k, abi.OldMapBucketCount*uintptr(t.KeySize))\n        }\n\n        // 写入目标桶的 tophash / key / value\n        dst.b.tophash[dst.i\u0026(abi.OldMapBucketCount-1)] = top\n        if t.IndirectKey() { *(*unsafe.Pointer)(dst.k) = k2 } else { typedmemmove(t.Key, dst.k, k) }\n        if t.IndirectElem(){ *(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e) } else { typedmemmove(t.Elem, dst.e, e) }\n        dst.i++\n        dst.k = add(dst.k, uintptr(t.KeySize))\n        dst.e = add(dst.e, uintptr(t.ValueSize))\n    }\n}\n```\n\n（3） 辅助迁移收尾，推进标识位的更新\n\n```go\nif h.flags\u0026oldIterator == 0 \u0026\u0026 t.Bucket.Pointers() {\n    b := add(h.oldbuckets, oldbucket*uintptr(t.BucketSize))\n    ptr := add(b, dataOffset)\n    n := uintptr(t.BucketSize) - dataOffset\n    memclrHasPointers(ptr, n)\n}\n\nif oldbucket == h.nevacuate {\n    advanceEvacuationMark(h, t, newbit)\n}\n```\n\n### 2.2.6 遍历\n\n我们先来看 go-map 中一个经典的问题，关注下面的代码，请问输出是什么？\n\n```go\nfunc main() {\n\tcache := make(map[int]string, 16)\n\n\tcache[1] = \"A\"\n\tcache[2] = \"B\"\n\tcache[3] = \"C\"\n\tcache[4] = \"D\"\n\n\n\tfor k, v := range cache {\n\t\tprintln(k, \" + \", v)\n\t}\n}\n```\n\n**答曰：** 不知道～\n\n```go\n❯ go build -o main main.go\n❯ ./main\n4  +  D\n1  +  A\n3  +  C\n2  +  B\n❯ ./main\n2  +  B\n4  +  D\n3  +  C\n1  +  A\n```\n\n我们发现同一段代码，遍历顺序却随机，答案就藏在 map 的迭代器中，**go 团队设计迭代 map 的时候故意设计成了随机的，就是为了消除程序对有序遍历 map 的依赖，这主要是对可移植性和安全性的考量。如果程序依赖固定的顺序迭代 map，那这段代码可能在不同的硬件平台上的执行结果不同，导致代码在不同平台上无法运行**。具体可以看下面链接的一些内容。  \nhttps://stackoverflow.com/questions/55925822/why-are-iterations-over-maps-random  \n如下图，迭代器会随机定位到某个 Bucket 的某个溢出桶的某个位置开始迭代，从而知道随机性。\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251031000913089.png)\n\n---\n\n接着我们来看看迭代部分的代码，通过类似的定位手段，可以找到其底层是基于 `runtime.mapiterinit()` 初始化一个迭代器，然后使用 `runtime.mapiternext()` 方法进行迭代的，我们展开来看看，首先来看看初始化迭代器。  \n首先先介绍一下 hmap 的迭代器的结构 hiter\n\n```go\ntype hiter struct {\n\t// 当前迭代的 key 的地址\n\tkey         unsafe.Pointer\n\t// 当前迭代的 val 的地址\n\telem        unsafe.Pointer\n\t// 当前 map 的元信息\n\tt           *maptype\n\t// hmap 结构体指针\n\th           *hmap\n\t// 桶数组快照\n\tbuckets     unsafe.Pointer\n\t// 当前所在的溢出桶（bmap）\n\tbptr        *bmap\n\t// 新老桶数组对应的溢出桶；\n\toverflow    *[]*bmap\n\toldoverflow *[]*bmap\n\t// 起始桶索引\n\tstartBucket uintptr\n\t// 桶内起始偏移\n\toffset      uint8\n\t// 是否已经 从桶数组尾部绕回 到开头\n\twrapped     bool\n\t// 迭代开始时的 B 值\n\tB           uint8\n\t// 当前所处的桶内的索引\n\ti           uint8\n\t// 当前所处的桶（bucket）索引，这个要和上面的 bptr 做区分\n\tbucket      uintptr\n\t// 用于在 扩容/渐进搬迁 时校验：当 oldbuckets 尚存在，读路径/迭代需要按规则优先从旧桶读未搬迁元素；\n\t// checkBucket 协助判断该看旧还是新、或是否跳过已 evacuated 的桶。\n\tcheckBucket uintptr\n\tclearSeq    uint64\n}\n```\n\n```go\n//go:linkname mapiterinit\nfunc mapiterinit(t *maptype, h *hmap, it *hiter) {\n\t// ...\n\n\t// 如果 map 为空，直接返回\n\tit.t = t\n\tif h == nil || h.count == 0 {\n\t\treturn\n\t}\n\n\tif unsafe.Sizeof(hiter{}) != 8+12*goarch.PtrSize {\n\t\tthrow(\"hash_iter size incorrect\")\n\t}\n\t// 初始化 iter 中的一些值\n\tit.h = h\n\tit.clearSeq = h.clearSeq\n\n\t// grab snapshot of bucket state\n\tit.B = h.B\n\tit.buckets = h.buckets\n\tif !t.Bucket.Pointers() {\n\t\th.createOverflow()\n\t\tit.overflow = h.extra.overflow\n\t\tit.oldoverflow = h.extra.oldoverflow\n\t}\n\n\t// 决定 it 从哪里出发，这里明显可以看到使用了一个随机的初始点\n\t// 这里定随机桶和随机桶内的偏移量\n\tr := uintptr(rand())\n\tit.startBucket = r \u0026 bucketMask(h.B)\n\tit.offset = uint8(r \u003e\u003e h.B \u0026 (abi.OldMapBucketCount - 1))\n\n\t// iterator state\n\tit.bucket = it.startBucket\n\n\t// 这里会设置 hmap，告知当前运行时存在迭代器\n\t// 其他迁移/删除逻辑会参考这个标记做进一步安全性的检查，防止出错\n\tif old := h.flags; old\u0026(iterator|oldIterator) != iterator|oldIterator {\n\t\tatomic.Or8(\u0026h.flags, iterator|oldIterator)\n\t}\n\t// 执行迭代流程\n\tmapiternext(it)\n}\n```\n\n（1）空值和安全性校验\n\n```go\nit.t = t\nif h == nil || h.count == 0 {\n\treturn\n}\n\nif unsafe.Sizeof(hiter{}) != 8+12*goarch.PtrSize {\n\tthrow(\"hash_iter size incorrect\")\n}\n```\n\n（2）初始化 iter 中的一些值\n\n```go\nit.h = h\nit.clearSeq = h.clearSeq\n// grab snapshot of bucket state\nit.B = h.B\nit.buckets = h.buckets\nif !t.Bucket.Pointers() {\n\th.createOverflow()\n\tit.overflow = h.extra.overflow\n\tit.oldoverflow = h.extra.oldoverflow\n}\n```\n\n（3）这里不然看出，决定迭代的起始桶和桶内偏移是用 `rand()` 选择的，这也就对应了一开始的部分\n\n```go\n// 决定 it 从哪里出发，这里明显可以看到使用了一个随机的初始点\n// 这里定随机桶和随机桶内的偏移量\nr := uintptr(rand())\nit.startBucket = r \u0026 bucketMask(h.B)\nit.offset = uint8(r \u003e\u003e h.B \u0026 (abi.OldMapBucketCount - 1))\n// iterator state\nit.bucket = it.startBucket\n\n// 这里会设置 hmap，告知当前运行时存在迭代器\n// 其他迁移/删除逻辑会参考这个标记做进一步安全性的检查，防止出错\nif old := h.flags; old\u0026(iterator|oldIterator) != iterator|oldIterator {\n\tatomic.Or8(\u0026h.flags, iterator|oldIterator)\n}\n// 执行迭代流程\nmapiternext(it)\n```\n\n接下来进入迭代流程\n\n```go\n//go:linkname mapiternext\nfunc mapiternext(it *hiter) {\n\th := it.h\n\t// ...\n\t// 基础并发检查\n\tif h.flags\u0026hashWriting != 0 {\n\t\tfatal(\"concurrent map iteration and map write\")\n\t}\n\t// 分别获取 map 元数据，桶数组，当前的溢出桶，桶内下标等\n\tt := it.t\n\tbucket := it.bucket\n\tb := it.bptr\n\ti := it.i\n\tcheckBucket := it.checkBucket\n\nnext:\n\tif b == nil {\n\t\t// 如果当前处于开始遍历的位置，并且已经遍历完一轮，则结束迭代\n\t\tif bucket == it.startBucket \u0026\u0026 it.wrapped {\n\t\t\tit.key = nil\n\t\t\tit.elem = nil\n\t\t\treturn\n\t\t}\n\t\t// 如果当前处于扩容状态，则将桶设置为旧桶\n\t\tif h.growing() \u0026\u0026 it.B == h.B {\n\t\t\toldbucket := bucket \u0026 it.h.oldbucketmask()\n\t\t\tb = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))\n\t\t\tif !evacuated(b) {\n\t\t\t\t// 如果旧桶还没有迁移结束，则需要设置检查桶\n\t\t\t\tcheckBucket = bucket\n\t\t\t} else {\n\t\t\t\t// 旧桶迁移未结束，则不需要设置检查桶，并且转去搜索新桶\n\t\t\t\tb = (*bmap)(add(it.buckets, bucket*uintptr(t.BucketSize)))\n\t\t\t\tcheckBucket = noCheck\n\t\t\t}\n\t\t} else {\n\t\t\t// 不处于扩容状态，则无需设置检查桶\n\t\t\tb = (*bmap)(add(it.buckets, bucket*uintptr(t.BucketSize)))\n\t\t\tcheckBucket = noCheck\n\t\t}\n\t\tbucket++\n\t\t// 如果桶下标到头了，则设置回到开头，并设置遍历轮回的标记\n\t\tif bucket == bucketShift(it.B) {\n\t\t\tbucket = 0\n\t\t\tit.wrapped = true\n\t\t}\n\t\ti = 0\n\t}\n\t// 遍历桶内的槽位\n\tfor ; i \u003c abi.OldMapBucketCount; i++ {\n\t\toffi := (i + it.offset) \u0026 (abi.OldMapBucketCount - 1)\n\t\tif isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {\n\t\t\t// 空槽直接跳过\n\t\t\tcontinue\n\t\t}\n\t\t// 获取到槽内的 key\n\t\tk := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.KeySize))\n\t\tif t.IndirectKey() {\n\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t}\n\t\t// 获取到槽内的 val\n\t\te := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+uintptr(offi)*uintptr(t.ValueSize))\n\t\t// 如果设置了检查旧桶，并且是增量扩容，此时需要判断这个 key 处于新桶中的原位置还是被放在了新桶中的新位置\n\t\t// （rehash 的时候用高位 0/1 来判断要送去新桶还是旧桶）\n\t\tif checkBucket != noCheck \u0026\u0026 !h.sameSizeGrow() {\n\t\t\tif t.ReflexiveKey() || t.Key.Equal(k, k) {\n\t\t\t\t// If the item in the oldbucket is not destined for\n\t\t\t\t// the current new bucket in the iteration, skip it.\n\t\t\t\thash := t.Hasher(k, uintptr(h.hash0))\n\t\t\t\tif hash\u0026bucketMask(it.B) != checkBucket {\n\t\t\t\t\t// 如果这个 key 处于新桶，则跳过，检查下一个 key，等检查新桶的时候在输出\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif checkBucket\u003e\u003e(it.B-1) != uintptr(b.tophash[offi]\u00261) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif it.clearSeq == h.clearSeq \u0026\u0026\n\t\t\t((b.tophash[offi] != evacuatedX \u0026\u0026 b.tophash[offi] != evacuatedY) ||\n\t\t\t\t!(t.ReflexiveKey() || t.Key.Equal(k, k))) {\n\t\t\t// 如果当前 key 经过检查可以确定是可信的，那直接用这个数据\n\t\t\tit.key = k\n\t\t\tif t.IndirectElem() {\n\t\t\t\te = *((*unsafe.Pointer)(e))\n\t\t\t}\n\t\t\tit.elem = e\n\t\t} else {\n\t\t\t// 如果 map 在迭代期间发生了一些增删改等，则调用方法获取对应的值\n\t\t\trk, re := mapaccessK(t, h, k)\n\t\t\tif rk == nil {\n\t\t\t\tcontinue // key has been deleted\n\t\t\t}\n\t\t\tit.key = rk\n\t\t\tit.elem = re\n\t\t}\n\t\t// 更新迭代器进度\n\t\tit.bucket = bucket\n\t\tif it.bptr != b { // avoid unnecessary write barrier; see issue 14921\n\t\t\tit.bptr = b\n\t\t}\n\t\tit.i = i + 1\n\t\tit.checkBucket = checkBucket\n\t\treturn\n\t}\n\tb = b.overflow(t)\n\ti = 0\n\tgoto next\n}\n```\n\n（1）如果当前在起始点，则判断当前是不是以及循环一轮了，如果是，则代表迭代结束，直接返回\n\n```go\nif bucket == it.startBucket \u0026\u0026 it.wrapped {\n\tit.key = nil\n\tit.elem = nil\n\treturn\n}\n```\n\n（2）确定要遍历的桶数组，如果当前正处于增量扩容阶段，则需要去旧桶中检查，否则直接在新桶中检查就行。这里有两个比较关键的点：\n\n1. 其实这里比较关键的代码是 `oldbucket := bucket \u0026 it.h.oldbucketmask()`，**这句代码的含义是，取 bucket 的低 B - 1 位，通过低 B 位能判断出这个桶的旧桶位置。举个例子，假设原来的 B 为 3（8 个桶），翻倍后 B 位 4（16 个桶）。新桶的第一个桶下标是 0，第九个桶下标是 8，这两个桶的数据按照扩容原则，应该都属于旧桶中的的 0 号桶，比如此时 0 号桶低 3 位（B - 1）是 000，8 的低 3 位也是 000，这样就能确定他们都属于 0 号桶。**\n2. bucket 字段始终指向新桶，而 b 字段有可能指向新桶也有可能指向旧桶。\n\n```go\nif h.growing() \u0026\u0026 it.B == h.B {\n\toldbucket := bucket \u0026 it.h.oldbucketmask()\n\tb = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))\n\tif !evacuated(b) {\n\t\t// 如果旧桶还没有迁移结束，则需要设置检查桶\n\t\tcheckBucket = bucket\n\t} else {\n\t\t// 旧桶迁移未结束，则不需要设置检查桶，并且转去搜索新桶\n\t\tb = (*bmap)(add(it.buckets, bucket*uintptr(t.BucketSize)))\n\t\tcheckBucket = noCheck\n\t}\n} else {\n\t// 不处于扩容状态，则无需设置检查桶\n\tb = (*bmap)(add(it.buckets, bucket*uintptr(t.BucketSize)))\n\tcheckBucket = noCheck\n}\n```\n\n（3）遍历桶内数据，获取到 kv\n\n```go\nfor ; i \u003c abi.OldMapBucketCount; i++ {\n\t\toffi := (i + it.offset) \u0026 (abi.OldMapBucketCount - 1)\n\t\t// ...\n```\n\n（4）如果在增量扩容的时候，因为桶数组长度翻倍，所以原来的 bucket 的一些数据可能会被 rehash 到新的 bucket 的溢出桶中，所以此时获取到 key 后，要检查其是否属于当前旧桶，如果这个 key 未来会被分流到新桶中，则跳过，等遍历到新桶的时候，再输出。\n\n```go\nif checkBucket != noCheck \u0026\u0026 !h.sameSizeGrow() {\n\tif t.ReflexiveKey() || t.Key.Equal(k, k) {\n\t\thash := t.Hasher(k, uintptr(h.hash0))\n\t\tif hash\u0026bucketMask(it.B) != checkBucket {\n\t\t\t// 如果这个 key 处于新桶，则跳过，检查下一个 key，等检查新桶的时候在输出\n\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\tif checkBucket\u003e\u003e(it.B-1) != uintptr(b.tophash[offi]\u00261) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\t}\n\tif it.clearSeq == h.clearSeq \u0026\u0026\n\t\t((b.tophash[offi] != evacuatedX \u0026\u0026 b.tophash[offi] != evacuatedY) ||\n\t\t!(t.ReflexiveKey() || t.Key.Equal(k, k))) {\n\t\t// 如果当前 key 经过检查可以确定是可信的，那直接用这个数据\n\t\tit.key = k\n\t\tif t.IndirectElem() {\n\t\t\te = *((*unsafe.Pointer)(e))\n\t\t}\n\t\tit.elem = e\n\t} else {\n\t\t// 如果 map 在迭代期间发生了一些增删改等，则调用方法获取对应的值\n\t\trk, re := mapaccessK(t, h, k)\n\t\tif rk == nil {\n\t\t\tcontinue // key has been deleted\n\t\t}\n\t\tit.key = rk\n\t\tit.elem = re\n\t}\n```\n\n（5）更新迭代器进度\n\n```go\n// 更新迭代器进度\n\tit.bucket = bucket\n\tif it.bptr != b {\n\t\tit.bptr = b\n\t}\n\tit.i = i + 1\n\tit.checkBucket = checkBucket\n\treturn\n}\nb = b.overflow(t)\ni = 0\ngoto next\n```\n\n# 彩蛋\n\n为什么不用 `interface{}` 来表示泛型呢？这样代码可读性会提高不少。\n\n1. 规范性角度考虑：`interface{}` 规范性上来讲是一种动态类型的容器（鸭子类型），是运行是多态，真正的泛型是编译时推断类型。所以用 `interface{}` 表达泛型本身就不规范。\n2. 存储角度考量：我们来看看当使用 `interface{}` 来组成这个结构体，他们之间的差距会有多大？我们可以简单试探一下。\n\n```go\npackage main\n\nimport (\n\t\"unsafe\"\n)\n\ntype bmap struct {\n\ttophash [8]uint8\n\tkey [8]interface{}\n\tval [8]interface{}\n\tnextBmap *bmap\n}\n\ntype bmap1 struct {\n\ttophash [8]uint8\n\tkey [8]uint8\n\tval [8]uint8\n\tnextBmap *bmap\n}\n\nfunc main() {\n\tvar map1 bmap\n\tvar map2 bmap1\n\n\tprintln(unsafe.Sizeof(map1))\n\tprintln(unsafe.Sizeof(map2))\n}\n```\n\n输出如下\n\n```shell\n❯ go build -o main main.go\n❯ ./main\n272\n32\n```\n\n在这种极端的情况下，二者所占据的内存差异极大，我想这是一个为什么不用 `interface{}` 的点。而且 go 底层对不同类型的 kv 对的 map 都做了特殊优化，用 `interface{}` 是又慢又重。\n## 参考资料\nhttps://pkg.go.dev/gitea.wit.com/jcarr/iter/abi#OldMapBucketCountBits  \nhttps://draven.co/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/#%E5%93%88%E5%B8%8C%E5%87%BD%E6%95%B0  \nhttps://stackoverflow.com/questions/55925822/why-are-iterations-over-maps-random  \nhttps://zhuanlan.zhihu.com/p/597483155  \nhttps://www.cnblogs.com/baxianhua/p/11699068.html\n","date":"2025-10-29","path":"/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/go/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-go-map%E4%B8%8A/","permalink":"http://xiaoshidui.top/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/go/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-go-map%E4%B8%8A/","title":"深入浅出 Go-Map（上）"},{"content":"\n## 什么是摘樱桃？\n\n在分支协作中，出现以下这种情况，假设我的 main 分支只想要 feat 分支的 E 或者 F 的改动，此时就需要用到像摘樱桃一样给那几个提交摘过来（一个个小提交就像小樱桃），也就是 cherry-pick。\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251028010455368.png)\n理解起来不难，但在实际操作中，还是有一些小问题的。我把目前摘樱桃的情况归为三类：\n\n1. 第一类是无冲突直接摘，例如 E 提交创建了一个新文件，这时 main 直接摘 E 过来是没问题的。\n2. 第二类是假设 F 是基于 E 的改动，但是我摘的时候，只摘了 F 过来，没有摘 E，此时摘樱桃操作会卡住。\n3. 第三类是你摘过来的提交和你本地的提交产生了冲突，此时需要解决冲突，cherry-pick 操作会卡住。\n\n## 无冲突直接摘\n\n这个没什么好说的，很简单，所以这里介绍一下几个 cherry-pick 的命令，分别是\n\n```git\ngit cherry-pick A // 只摘提交 A\ngit cherry-pick A..B // 摘提交(A, B]\ngit cherry-pick A^..B // 摘提交[A, B]\ngit cherry-pick --abort // cherry-pick 操作终止，回滚\ngit cherry-pick --continue // 解决冲突后，cherry-pick 操作继续执行\n```\n\n## 所摘提交的依赖提交未摘\n\n现在 feat 分支中有一个 main 分支中不存在的文件 READMEX.md，feat 中分别有两个提交，提交 A 是创建文件并写入一些内容，提交 B 是在文件中追加了一些内容。\n\n```\n# gitTest base02 cherry-pick 提交 4 // A\n\n\n# gitTest base02 cherry-pick 提交 5 // B\n```\n\n此时，假设我在分支 main 上直接 pick B 会怎么样？这时会报错，提示我们提交 B 所依赖的前置提交不存在，该怎么解决呢？\n\n```\n❯ git cherry-pick 722ba9ec56966187e96\nCONFLICT (modify/delete): READMEX.md deleted in HEAD and modified in 722ba9e (base02 cherry-pick test5).  Version 722ba9e (base02 cherry-pick test5) of READMEX.md left in tree.\nerror: could not apply 722ba9e... base02 cherry-pick test5\nhint: After resolving the conflicts, mark them with\nhint: \"git add/rm \u003cpathspec\u003e\", then run\nhint: \"git cherry-pick --continue\".\nhint: You can instead skip this commit with \"git cherry-pick --skip\".\nhint: To abort and get back to the state before \"git cherry-pick\",\nhint: run \"git cherry-pick --abort\".\n```\n\n两种方案：\n\n1. --abort（计划有变，终止交易）\n2. 先摘 A 再摘 B\n3. 把 AB 全摘过来\n4. 删库跑路\n   这里我们看第三种方案的具体实操，这会就需要用到我们前面说的命令了\n\n```\ngit cherry-pick A..B // 摘提交(A, B]\ngit cherry-pick A^..B // 摘提交[A, B]\n```\n\n这里我们选择第二个，因为我们需要 A 提交\n\n```\n❯ git cherry-pick 3ce4aae8d99d^..722ba9ec56966187e96\n[detached HEAD 942b448] base02 cherry-pick test4\n Date: Tue Oct 28 00:43:20 2025 +0800\n 1 file changed, 1 insertion(+)\n create mode 100644 READMEX.md\n[detached HEAD 7d07657] base02 cherry-pick test5\n Date: Tue Oct 28 00:43:31 2025 +0800\n 1 file changed, 4 insertions(+), 1 deletion(-)\n```\n\n顺利执行，main 分支成功创建文件并且 Get 到了提交内容。\n\n## 所摘提交与本地提交冲突\n\n下面是 main 分支文件 A 的内容：\n\n```\n# gitTest Base1 第一次提交\n# gitTest Base2 第二次提交\n# gitTest Base3 第三次提交\n# gitTest Base1 第四次提交\n# gitTest Base1 第五次提交\n# gitTest Base1 第六次提交\n# gitTest Base1 测试多分支变基追溯\n# gitTest Base1 测试多分支变基追溯02\n\n# gitTest main cherry-pick 提交 1\n# gitTest main cherry-pick 提交 2\n```\n\n下面是 feat 分支文件 A 的内容：\n\n```\n# gitTest Base1 第一次提交\n# gitTest Base2 第二次提交\n# gitTest Base3 第三次提交\n# gitTest Base1 第四次提交\n# gitTest Base1 第五次提交\n# gitTest Base1 第六次提交\n# gitTest Base1 测试多分支变基追溯\n# gitTest Base1 测试多分支变基追溯02\n\n# gitTest Base1 cherry-pick 提交 6 测试冲突\n```\n\ngitTest Base1 cherry-pick 提交 6 测试冲突 是 feat 的最新提交，我们可以看出与 main 分支的 A 中的后两行发生了冲突，此时执行 cherrpick，将 feat 的提交摘过来，会发生什么？如下\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251028012201978.png)\n只需要解决冲突，然后将冲突文件加入到暂存区，然后执行继续摘就行\n\n```git\ngit add \u003cfile_name\u003e\ngit cherry-pick --continue\n```\n\n或者如果你想中止这次操作，可以使用回滚\n\n```git\ngit cherry-pick --abort\n```\n\n## 总结\n\n其实 cherry-pick 是很好理解的，但是要注意一些小细节，比如说在解决冲突后，要把冲突文件添加到暂存区；以及摘来的提交实际上与原分支上的 CommitID 是不同的，这些等等都需要我们在实践中不断学习。加油，LLL。\n","date":"2025-10-28","path":"/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/git/git-%E4%B9%8B-cherry-pick/","permalink":"http://xiaoshidui.top/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/git/git-%E4%B9%8B-cherry-pick/","title":"Git 之 Cherry-Pick"},{"content":"\n    老 merge 了，但 rebase 弱的一逼，面试被问到，遂复盘\n\n## 场景\n\n小 A 在 feat1 分支开发，他肯定不能直接在 main 上做修改（为了保护项目的安全性），此时，为了防止大量的冲突，小 A 每次开发前都要拉去 main 分支的代码来看看，没啥冲突就直接合入，这样能有效避免积压大量的冲突。  \n此时就有问题了，git 中有两种合入代码的方式，分别是 merge 和 rebase，对应命令如下\n\n```git\ngit merge origin/main\ngit rebase origin/main\n```\n\n到底用哪一个呢？小 A 挠挠头。  \n这里先给出一个方案，如果你是 main / master 分支的维护者，尽量使用 Merge，如果你是要维护自己的分支，可以用 Rebase，原因我们娓娓道来。\n\n## GitGraph\n\nGitGraph 是直观的 Git 多分支提交记录的展示，下面是一张示意图\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251027174802542.png)\n在 GitGraph 中一条分叉就是一个分支，比如说上图，从 C 这个提交分出了一个 Feat 分支，也就是说，Feat 分支拥有含 C 之前的所有 Main 的提交，从 C 之后，Main 和 Feat 形同陌路。\n\n## Merge 合入\n\nOK，我们先简单看看，基于上面这张 GitGraph，如果说在小 A 执行 merge 操作，会发什么？\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251027181459117.png)\n很直观，如果执行 merge，会基于 main 分支的最新提交和 feat 分支的最新提交，进行合并，然后形成一个新的提交，非常直观。\n\n## Rebase 合入\n\n那么 Rebase 合入是怎么进行的呢？\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251027183216003.png)\n可以看出，rebase 的合入，是将 main 中多出来的提交信息插入到 feat 分支的 GitGraph 中，直观来看就是整个 feat 分支的提交记录更加清爽直观了，没有那么多分叉了。但是这里有个细节就是，F‘ 和 G’，这里为什么不是 F 和 G 呢？这就是 Rebase 的危险所在，**他会重铸你的 GitLog**。  \n我们知道，我们每次的 Git Commit 都会有一个 Hash 值来唯一标识一个分支上的一次提交，而用 Rebase 合入的时候，会将分支上的最新的 main 还未同步的提交进行”修改“，直观来看就是 Commit 的 Hash 值会改变。  \nRebase 合入时， GitCommit 的这个 Hash 值为什么会改变呢？我们先了解一下这个 Hash 值是怎么算出来的。**Git 通过对每个提交包含的元数据（如父提交、作者、提交者、日期和提交信息）以及内容（目录树的快照）进行 SHA-1 哈希算法计算得出一次提交的 Hash 值。**  \n那么回到问题的本源就能理解了，因为 rebase 要维护线性的提交记录，所以会导致执行 rebase 的分支的最新提交的父提交修改（至少），进而引发 Hash 值的改变。\n\n## 孰优孰劣？\n\n既然 rebase 能让我们的 git 提交信息更加清爽，那我们猛 rebase 不就好了？merge 有啥用？其实不然，收回伏笔，在一般开发中，我们比较推荐的是，在开发分支上 rebase，在 main / master 分支上 merge。  \n假设现在有这样一种情况：\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251027220438077.png)\n我们可以看出 Feat2 分支比 main 多了一个 F 提交，而 Feat1 是建立在 Main 分支的 K 提交之上的。此时，我们用 rebase 合入 Feat2 的 F 提交。Main 分支会变成下面这样，进而造成一些麻烦。\n![image.png](https://img.xiaoshidui.top/blog-pic/images/20251027220657106.png)\n\n## 结论\n\n综上，其实对于自己的分支，你怎么完都无所谓，用 rebase 会让你的分支提交信息更加简洁，但是如果你掌管了 main / master 分支，那最好用 merge 合入代码，以防一些不必要的麻烦。（留个种子，交互式变基等 rebase 的扩展操作后面还要开一章学习）\n","date":"2025-10-27","path":"/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/git/%E8%A7%A3%E6%9E%90-rebase--merge-%E5%90%88%E5%85%A5/","permalink":"http://xiaoshidui.top/posts/%E4%B8%93%E4%B8%9A/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/git/%E8%A7%A3%E6%9E%90-rebase--merge-%E5%90%88%E5%85%A5/","title":"解析 Rebase \u0026 Merge"}]