Map 面试题
Map 面试题
1. Go 语言 Map 的底层实现原理是怎样的?
map 就是一个 hmap 的结构。Go Map 的底层实现是一个哈希表。它在运行时表现为一个指向 hmap 结构体的指针,hmap 中记录了桶数组指针 buckets、溢出桶指针以及元素个数等字段。每个桶是一个 bmap 结构体,能存储 8 个键值对和 8 个tophash,并有指向下一个溢出桶的指针 overflow。为了内存紧凑,bmap 中采用的是先存 8 个键再存 8 个值的存储方式。
hmap 结构体定义如下:
1 | type hmap struct { |
2. Go 语言 Map 的遍历是有序的还是无序的?
无序
map 每次遍历,都会从一个随机值序号的桶开始遍历,在每个桶中,再从按照
之前选定随机槽位开始遍历,所以是无序的
3. Go 语言 Map 的遍历为什么要设计成无序的?
Go 的 map 底层是哈希表(hash table),其结构特点决定了遍历顺序:
- key 经过 hash 计算后分布到不同 bucket
- bucket 内部存储 key/value
- 遍历时是按 bucket 顺序 + 随机起点
👉 Go runtime 在设计上还额外做了:
- 随机化遍历起点
- 避免开发者依赖顺序
4. Map 如何实现顺序读取?
将 Map 的键(key)取出来放到一个切片(slice)中,对切片进行排序,然后按照排序后的顺序访问 Map 中的值(value)。示例代码如下:
1 | package main |
5 Go 语言的 Map 是否是并发安全的?
不是
在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志已经被置位(说明已经有 goroutine 在写),则直接 throw(runtime 触发 fatal error,不可被 recover 捕获)。赋值和删除函数在检测到写标志为复位状态后,会先将写标志位置位,再进行后续操作。
6. Map 的 Key 一定要是可比较的吗?为什么?
一定是
Go 语言的 Map 要求 Key 必须是可比较的(comparable),这是因为 Map 内部需要通过 Key 来计算哈希值并进行比较,以确定 Key 的位置和是否存在。不可比较的类型(如切片、函数、Map 本身等)无法进行哈希计算和比较,因此不能作为 Map 的 Key。
7. Go 语言 Map 的扩容时机是怎样的?
向 map 插入新 key 的时候,会进行条件检测,符合下面这2个条件,就会触发扩容:
- 装载因子超过阈值(源码里定义的阈值是 6.5),触发双倍扩容(B+1,buckets 数量翻倍)。
- overflow 的 bucket 数量过多(触发等量扩容,B不变,仅重新整理键值对让分布更紧凑):
- 当 B < 15,也就是 bucket 总数 2^B < 2^15 时,如果 overflow 的 bucket 数量超过 2^B
- 当 B >= 15 ,也就是 bucket 总数 2^B >= 2^15,如果 overflow 的 bucket 数量超过 2^15。
8. Go 语言 Map 的扩容过程是怎样的?
Go map 扩容不是“整体复制”,而是:
-
创建新 bucket(通常翻倍)
- oldbuckets → newbuckets
- bucket 数量翻倍
-
渐进式搬迁(Incremental Evacuation)
Go 不会一次性迁移全部数据,而是:
- 每次 map 操作顺便搬一部分 bucket
机制:
- 访问 / 插入时触发 growWork
- 每次搬 1~2 个 bucket
- oldbuckets 逐步清空
- 扩容期间的状态
扩容时 map 内部会同时存在:
1 | oldbuckets -> 旧数据 |
查找逻辑:先查 oldbuckets,再查 newbuckets
9. 可以对 Map 的元素取地址吗?
不可以
这样设计主要是因为 map 一旦发生扩容, key 和 value 的位置就会改变,之前保存的地址也就失效了
10. Map 中删除一个 key,它的内存会释放么?
不会
delete(m, key) 只是把 key 和 value 对应的内存块标记为“空闲”,让它们的内容可以被后续的垃圾回收处理掉。只有当 map 失去所有引用后整个 map 才会被垃圾回收后释放
11. Map 可以边遍历边删除吗?
-
多个协程同事读写同一个 map:不可以,如果被检测到会直接 panic
-
发生在同一个协程内:可以,但是遍历的结果可能不会是相同的
参考资料: