Go map通过哈希值低B位定位bucket,B为uint8表示bucket总数2^B,定位公式为hash&(1

bucket 定位只用哈希值的低 B 位
Go map 查找 key 时,第一步不是遍历所有 bucket,而是直接算出它该去哪个 bucket —— 这个过程不依赖比较,纯位运算,极快。关键就落在 B 字段上:B 是一个 uint8,表示当前 map 的 bucket 数量是 2^B。定位公式就是 hash & (1,等价于取哈希值的最低 <code>B 位。
比如 B = 4,bucket 总数是 16,那么任何 key 的哈希值只需取低 4 位(范围 0–15),就能直接命中对应 bucket 索引。这不是取模(% 16),而是位与,避免除法开销。
常见误区:以为 hash 值高位或中间位参与 bucket 分配 —— 实际完全不用。只要 B 不变,相同 key 永远落在同一个 bucket,这是 map 稳定性的基础。
tophash 高 8 位用于桶内快速筛键
进了 bucket 后,不急着逐个比对 key,先看 tophash 数组。它存的是每个 key 哈希值的高 8 位(即最高一个字节),共 8 个 slot。查找时先拿目标 key 的哈希高 8 位,跟 tophash[i] 对比。
如果 tophash[i] != 高8位,说明这个 slot 肯定不匹配,跳过;只有相等才继续做完整 key 比较。这能大幅减少字符串或结构体 key 的实际内存比对次数。
注意点:
-
tophash是 uint8 数组,溢出值(如 0x01)有特殊含义,不代表真实哈希,需跳过 - 空 slot 的
tophash值为 0,已删除 slot 为 0xfe,这两类都不参与匹配 - 高 8 位碰撞概率不低,所以它只是“快速筛选”,不是最终判定
overflow bucket 是链表式延展,不是二维数组
每个 bucket 最多装 8 个键值对。超过后不会扩容当前 bucket,而是分配一个新 bmap 作为 overflow bucket,并用指针串起来,形成单向链表。查找时必须按顺序遍历:主 bucket → overflow[0] → overflow[1] → … 直到 overflow == nil。
这意味着:
- 一旦某个 bucket 的 overflow 链过长(比如 >3 层),查找性能会明显下降 —— 因为要多次 cache miss 和指针跳转
- map 扩容(B 增大)时,overflow 链会被拆散重分布,但不是一次性全搬,而是渐进式迁移(由
nevacuate控制) - 不能假设所有 key 都在主 bucket 里,
len(m)大但某个 bucket 的 overflow 很深,是常见性能隐患
为什么 B 变化会导致所有 key 重新分配
B 增加 1(比如从 4→5),bucket 总数翻倍(16→32)。此时原来 hash & 0b1111 得到的索引,现在要算 hash & 0b11111。结果只可能有两种:原索引,或原索引 + 16(因为新增的高位 bit 是 0 或 1)。
也就是说,扩容不是“整体重排”,而是“一分为二”:每个旧 bucket 拆成两个新 bucket,key 根据新增的那一位 bit 值决定去哪边。这也是为什么扩容能渐进做 —— 每次只挪一个旧 bucket 里的 key 到两个新位置。
容易被忽略的是:哪怕你只插入一个 key,只要触发扩容,整个 map 的 bucket 分布逻辑就变了,所有后续查找都走新规则。而旧 bucket(oldbuckets)在迁移完成前仍要参与读操作,以保证并发安全。


















