Go Map 底层原理
虽然大家天天都在使用 map,但很多人对它的理解只停在'查得快'、'底层是哈希表'、'桶里有 8 个槽位'这几句。仅适用于非正式交流;但真到线上排查延迟抖动、锁竞争、内存占用、热点键冲突,这点认识往往是不够的。
本文旨在避免机械背诵,聚焦核心机制。主线只抓三件事:
- 它为什么能这么快;
- 它为什么会冲突;
- 它为什么需要扩容和同步;
在此说明,文章主体基于经典 Go map 实现,也就是大家最熟悉的 hmap / bmap / overflow bucket 这种;只会在关键处补上 Swiss Table 风格,Go 从 1.24 开始,就优化为了 Swiss Table 方案。
1. 一语戳破哈希表
哈希表解决的问题很直接:给我一个 key,我不想从头遍历到尾,我想尽快知道它大概率应该放在哪,查的时候也直接去那个位置附近找。
map[string]int 之所以平均查找复杂度能做到 O(1),靠的从而不是魔法,而是以下两步:
- 对 key 做哈希,得到一个哈希值。
- 用哈希值去定位存储位置。
可真正麻烦的地方不在'定位',而在'冲突'。不同 key 经过哈希后,完全可能落到同一个位置。于是,任何一个成熟的哈希表实现都得回答三个问题:
- 冲突来了怎么放
- 数据变多了怎么扩
- 并发访问时怎么保住一致性
Go map 的底层设计,本质上就是围着这三件事在做权衡。
2. 经典版:Go map 到底长什么样
Go 1.24 之前,用的都是经典版本。在此强调:本模块讲解的是 hmap / bmap / overflow bucket 这套设计。
2.1 hmap 解决什么问题
hmap 不负责一条一条存业务数据,它更像整个 map 的总控结构。它关心的是:
- 现在总共有多少元素
- 现在有多少个桶
- 当前桶数组在哪
- 如果正在扩容,旧桶数组在哪
- 旧桶搬迁到哪一步了
把源码结构体简化一下,大概就长这样:
hmap ├── count // 元素个数
├── B // 桶数量的对数,桶数约等于 2^B
├── buckets // 当前桶数组
├── oldbuckets // 扩容中的旧桶数组
└── nevacuate // 渐进式迁移进度
如果你把 map 想成一家公司,hmap 不是一线员工,它更像调度层。真正存 key/value 的,是下面的桶。
一句话总结:
hmap是经典 Go map 实现里的总控结构,真正的 key/value 主要放在 bucket 里,hmap负责管理桶数组、元素数量和扩容状态。
2.2 bmap 解决什么问题
bmap 可以理解成单个桶的结构。一个桶里最多放 8 组 key/value,这也是很多人记住的那个点。
把源码简化一下,大概长这样:
bmap ├── tophash[8]
├── key[0] ... key[]
├── value[] ... value[]
└── overflow *bmap


