Go map扩容在写入时触发,满足任一条件即启动:1.装载因子>6.5(count>2^B×6.5);2.溢出桶数>32768。分别触发翻倍扩容或等量扩容,并渐进式迁移数据。
什么时候触发 map 扩容
go 的 map 在写入(mapassign)时检查是否需要扩容,不依赖定时或后台协程。触发条件有两个,满足其一即标记为扩容中:
- 装载因子超过
6.5:即count > (2^B) * 6.5,B是当前桶数量的指数(len(buckets) == 1 << B) - 溢出桶过多:
noverflow > 2^15(也就是超过32768个已分配的溢出桶)
注意:noverflow 是近似值,不是精确计数;它只在新增溢出桶时递增,删除不会递减。所以即使删掉大量元素,只要曾经用过很多溢出桶,仍可能触发等量扩容。
翻倍扩容 vs 等量扩容的区别
两者都调用 hashGrow,但行为完全不同:
-
翻倍扩容:新建
2^(B+1)个桶(B自增 1),旧数据逐步迁移到新桶数组中,总容量翻倍 -
等量扩容:新建同样数量(
2^B)的桶,不改变B,只重新哈希所有 key 并搬迁——本质是“整理碎片”,降低溢出桶密度
判断逻辑在 hashGrow 内部:若因 noverflow 过高触发,则走等量扩容;若因装载因子超限,则走翻倍扩容。没有第三种路径。
扩容是渐进式迁移,不是一次性搬完
扩容开始后,hmap.oldbuckets 不为空,hmap.nevacuate 记录已迁移的旧桶索引。每次对 map 做读/写操作时,会顺带迁移最多 2 个 key-value 对(具体数量由 evacuate 控制)。
- 查找时:先查新桶,若未命中且
oldbuckets != nil,再查对应旧桶 - 写入时:直接写新桶;同时若该 key 所属的旧桶尚未迁移,就顺带把它挪过去
- 迭代时:
range会跳过已迁移的旧桶,避免重复遍历
这意味着扩容期间 map 仍可安全使用,但性能略降(多一次旧桶检查 + 搬迁开销)。整个过程直到 nevacuate == 2^B 才结束,oldbuckets 被置空,nevacuate 归零。
容易被忽略的关键细节
等量扩容看似“没扩容”,实则影响显著:
- 它不改变
B,但会重置noverflow为 0(因为新桶数组里暂无溢出桶) - 它强制对所有 key 重新哈希,可能导致原本集中在少数桶的 key 分散开来,缓解局部碰撞
- 如果 map 经历频繁增删、key 分布极不均匀,等量扩容可能反复触发——这是典型的空间换时间策略,代价是 CPU 时间,换来更稳定的访问延迟
真正难调试的是那种“内存占用高但 count 很小”的 map,大概率正卡在等量扩容循环里:旧溢出桶堆积 → 触发等量扩容 → 搬迁后新桶又快速产生溢出 → 再次触发……此时应检查 key 的哈希分布或考虑预分配合适大小。


















