大厂面经 | 小米 Go 实习一面:map 基本原理

大家好,我是老周,今天要和大家分享的是小米 Go 语言实习一面中关于 map 原理的面试题,一个非常经典的面试题,我们会从Map定义、应用场景、它的局限性、底层原理等角度和大家进行分享。

想要详细了解此篇内容的同学可以关注小破站:*********,解锁相应视频讲解以及300+大厂面经物料(附答案讲解)、golang学习物料等,希望能对面试的同学有帮助,面试遇到的问题也可以找老周解答,感谢支持关注!

小米 Go 实习一面:map 基本原理:

什么是 map

Map 是一种数据结构,直观理解为键值(Key-Value)结构,通过 Key 可以快速定位并获取对应的 Value,是编程中常用的数据存储与查询工具。

map 的特点及应用场景

(一)核心特点

  1. 快速查找:map 的平均时间复杂度为 O (1),通过一次计算就能快速找到 Key 对应的 Value,查询效率极高。
  2. Key 唯一性:每个 Key 在 map 中都是唯一的,不存在重复的 Key,若插入已存在的 Key,会覆盖原有 Value。

(二)典型应用场景

基于上述特点,map 的常见应用场景如下:

  • 计数器:利用 Key 唯一性,将需统计对象作为 Key,统计次数作为 Value,通过增减 Value 实现计数,如统计单词出现频率。
  • 配置管理:将配置项名称作为 Key,配置值作为 Value 存储,可快速读取、修改配置,如系统参数配置、服务连接配置。
  • 数组去重:遍历数组,将元素作为 Key 存入 map,因 Key 唯一,最终 map 的 Key 集合即为去重后的数组元素。
  • 缓存实现:将频繁访问的数据以 Key-Value 形式存入 map,后续访问直接从 map 获取,避免重复计算或频繁查询数据库,提升系统响应速度。

map 的限制

(一)无序性

  1. 无序原因:map 底层通过 Key 做哈希计算得到哈希值,再用哈希值对桶(bucket)数量取模,确定 Key 的存储位置。而 map 有底层扩容机制,元素数量较多时会触发扩容,扩容后桶数量改变,Key 取模结果变化,存储位置与扩容前不同,最终表现为无序。
  2. 有序处理思路:若业务需保证 Key 有序,无需在 map 底层额外维护有序结构(会降低多数无需有序场景的性能),建议在业务层处理,如需要有序展示时,对 Key 排序后再使用。

(二)非线程安全

  1. 安全情况划分
  • 线程安全场景:多个线程仅对 map 进行 “读” 操作,不会出现数据安全问题。
  • 线程不安全场景:存在 “读 - 写” 或 “写 - 写” 并发操作时,会出现数据竞争、数据不一致,严重时可能导致程序 Panic。
  1. 设计逻辑:大多数场景下,map 用于单线程环境。若为满足少数并发场景添加线程安全机制(如加锁、原子操作),会增加性能开销,降低单线程效率,因此 map 默认非线程安全。
  2. 开发注意事项:多线程环境使用 map 时,需手动保证线程安全,可使用 Go 语言的 sync.Map,或通过 sync.Mutex 加锁控制访问。

map 中 Key 访问 Value 的方式

(一)中括号访问

语法为 value := map[Key],直接通过 Key 获取 Value。若 Key 不存在,返回 Value 类型的零值,无法直接判断 Key 是否存在。

(二)带存在性判断的访问

语法为 value, ok := map[Key],返回两个值:

  • value:Key 对应的 Value(Key 不存在时,返回 Value 类型零值)。
  • ok:布尔值,true 表示 Key 存在,false 表示 Key 不存在。 适合需先判断 Key 是否存在,再进行后续操作的场景。

(三)迭代访问

通过循环遍历 map 所有 Key 和 Value,常见语法:

// 遍历Key和Value
for key, value := range map {
    // 业务逻辑
}

// 仅遍历Key
for key := range map {
    // 业务逻辑
}

// 仅遍历Value(忽略Key,用_表示)
for _, value := range map {
    // 业务逻辑
}

map 的底层原理

(一)底层核心结构:hmap 与 bmap

map 底层由 hmap(哈希表顶层结构)和 bmap(桶结构)组成,协同实现 Key-Value 的存储与查询。

1. hmap 的关键字段(基于源码)

2. bmap 的结构特点

  • 编译阶段:仅包含 topHash 字段,存储 Key 哈希值的高 8 位,用于桶内快速定位 Key,减少 Key 比较次数。
  • 运行阶段:动态添加三个字段:
  • Key 数组:存桶内所有 Key,长度为 8(每个桶默认最多存 8 个 Key-Value 对)。
  • Value 数组:存桶内所有 Value,与 Key 数组一一对应,长度 8。
  • overflow 指针:指向当前桶的溢出桶,桶存满 8 个元素后,新元素存入溢出桶,溢出桶存满则继续创建,形成链表。

(二)map 的存储逻辑(结合图示)

假设某 map 的 B=2(桶数量为 2^2=4),共 20 个元素,存储结构如下:

  1. 桶的分布:4 个主桶,每个主桶默认存 8 个元素,因总元素可能因哈希冲突需溢出桶,部分主桶关联溢出桶。
  2. 元素存储流程:
  • 对 Key 做哈希计算,得到完整哈希值。
  • 用哈希值的低 B 位(此处 B=2,即低 2 位)对桶数量(4)取模,确定 Key 应存的主桶。
  • 取哈希值的高 8 位存入主桶的 topHash 数组,同时将 Key 和 Value 分别存入主桶的 Key 数组和 Value 数组对应位置。
  • 若主桶存满 8 个元素,创建溢出桶,通过主桶的 overflow 指针指向溢出桶,将新元素存入溢出桶。

(三)哈希冲突的解决

哈希冲突指不同 Key 哈希计算后,取模结果相同,需存入同一主桶的情况。map 通过 “链地址法” 解决:

  1. 新 Key 需存的主桶已有元素时,先比较新 Key 的哈希值高 8 位与主桶 topHash 数组的值:
  • 若有相同值,再比较 Key 实际内容,Key 相同则覆盖 Value;Key 不同,继续在主桶内找空位置。
  • 若主桶无空位置,通过 overflow 指针找溢出桶,在溢出桶重复上述查找与存储,直至找到空位置或确定 Key 已存在。

(四)map 的扩容机制

满足以下任一条件时,触发扩容:

  1. 负载因子超标:负载因子 = 元素数量 / 桶数量,≥6.5 时,桶利用率过高,冲突概率大增,需扩容。
  2. 溢出桶过多:溢出桶过多会导致查找元素时遍历多个溢出桶,降低查询效率,需扩容。

扩容流程

  1. 准备阶段:创建新桶数组(数量为旧桶的 2 倍),hmap 的 oldbuckets 指向旧桶数组,buckets 指向新桶数组,nevacuate 初始化为 0(未开始迁移)。
  2. 迁移阶段:采用 “渐进式迁移”,每次读写 map 时,迁移 1 个或多个旧桶元素到新桶:
  • 对旧桶中每个 Key 重新计算哈希值,用新桶数量(旧桶的 2 倍)取模,确定在新桶的位置。
  • 将元素从旧桶迁移到新桶对应位置,迁移完成后 nevacuate 加 1。
  1. 完成阶段:所有旧桶元素迁移到新桶后,释放 oldbuckets 指向的旧桶数组,扩容完成。

(五)map 的插入操作(结合源码逻辑)

插入操作核心流程:

  1. 参数校验:map 为 nil(空指针)时,直接触发空指针异常(Panic)。
  2. 并发判断:检查 hmap 的 flags 标识位,若正在写操作,触发 Panic,禁止并发写。
  3. 哈希计算:根据 hash0(哈希因子)和 Key,计算 Key 的完整哈希值。
  4. 桶定位:用哈希值的低 B 位对当前桶数量取模,确定目标主桶;若正在扩容,先处理旧桶迁移,再定位新桶。
  5. 桶内查找与插入:
  • 遍历目标主桶及关联的溢出桶,比较 topHash 和 Key:
  • 找到相同 Key,更新对应 Value,插入结束。
  • 未找到相同 Key,记录主桶或溢出桶的空位置。
  • 找到空位置,将 Key 哈希值高 8 位存入 topHash,Key 和 Value 分别存入对应数组,count 加 1,插入结束。
  1. 扩容判断:未找到空位置(主桶和溢出桶均满),判断是否需扩容:
  • 需扩容:执行扩容流程后,重新执行插入(扩容后桶位置可能变化)。
  • 无需扩容:创建新溢出桶,将新元素存入新溢出桶,count 加 1,插入结束。
  1. 状态重置:修改 hmap 的 flags 标识位,解除写操作状态,完成插入。

以上就是本次的分享,希望能对准备面试的同学有帮助。想要详细了解此篇内容的同学可以关注小破站:*********,解锁相应视频讲解以及300+大厂面经物料(附答案讲解)、golang学习物料等,面试遇到的问题同时可以找老周解答,感谢支持关注!

#golang##程序员##计算机##大厂##面试#
全部评论

相关推荐

1. 你在三家比较大的公司都有实习经历,为什么一直在换呢?2. 你觉得这三家公司的技术体系有什么不同吗?3. 你们的三层缓存是怎么设计的?4. 第一层缓存(Kconf)是什么?它怎么工作的?5. 这一层缓存和 DB 怎么保持一致的?6. 你们的本地缓存过期策略是怎样的?为什么设置 5 秒?7. 你们更新 Redis 是通过 MQ,对吧?那 MQ 会丢消息吗?你们怎么保证不会丢?8. 你们用的 MQ 是什么?9. RocketMQ 能保证消息一定是在 DB 成功更新之后才投递出去吗?10. 你知道 RocketMQ 的事务消息具体是怎么实现的吗?11. 来写一段代码吧:两个线程交替打印奇偶数,打印到 100。12. 有没有可能存在多余的循环或空转的问题?13. 如果线程之间没有通信,会造成什么影响?要怎么改?(比如用阻塞+唤醒机制)14. 你可以用 **`synchronized`** / **`Object.wait/notify`** 或 **`Lock`** 来改写一下吗?15. 来一个设计题:如果要存储全球的行政区划数据(国家、省、市、区/县、街道),你会怎么设计?16. 不同国家层级不一样,这算一个难点,你怎么处理?17. 你会按层级来做表设计吗?这种设计可能存在哪些问题?18. 如果层级发生变化(比如新增一个层级),你的结构怎么应对?19. 有没有暴力一点的方案?(比如 JSON 存储)20. 那以“河北省”为例,你在这种 JSON 存储里会怎么表示?21. 你的 JSON 存储方案有什么缺点?22. 树型结构除了你这种方式,还有其他表达方式吗?23. 这种树形结构会面临哪些性能问题?比如查询跨级数据的时候怎么处理?24. 有没有更好的办法?能不能结合两种方式?25. 在读多写少场景,你会怎么优化?
发面经攒人品
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务