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


