当前位置: 首页 > news >正文

【Golang 面试 - 基础题】每日 5 题(七)

✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

 31. Go Map 查找

在 Go 语言中,使用 map 查找一个键值对的过程可以通过 map[key] 来完成,返回值是对应的值和一个表示是否存在的布尔值。

具体来说,如果 map 中存在该键,则返回对应的值和布尔值 true;如果不存在该键,则返回值类型的零值和布尔值 false。例如:

m := make(map[string]int)
m["apple"] = 1
value, ok := m["apple"]
if ok {fmt.Println(value) // 输出 1
}

另外,也可以直接使用一个值来获取键值对中的值,但是如果键值对中不存在该键,会返回该值类型的零值。例如:

m := make(map[string]int)
m["apple"] = 1
value := m["banana"]
fmt.Println(value) // 输出 0

需要注意的是,map 的键类型必须支持相等运算,例如,数字、字符串、指针、通道、接口类型、结构体类型等都是支持的,但是数组、切片、函数类型等不支持。

底层实现

在 Golang 中,Map 的查找是通过哈希表实现的。当程序执行 map 查找操作时,会先根据哈希函数将 key 转换成一个哈希值,然后在哈希表中查找该哈希值对应的桶 (bucket),再在桶中查找对应的键值对。

具体来说,当 Map 中的键值对数量超过一定阈值时,会触发自动扩容操作。扩容操作会重新分配更大的桶数组,并将原有的键值对重新哈希分布到新的桶中。

在查找时,Golang 的 Map 会先通过哈希值定位到对应的桶 (bucket),然后在桶中遍历链表(每个桶可能对应多个键值对)查找对应的键值对。在遍历链表的过程中,如果发现某个键值对的 key 与要查找的 key 相等,则返回该键值对的 value。

需要注意的是,如果 Map 中的键值对过多,桶中的链表会很长,查找时效率会降低,因此需要根据实际情况合理设置 Map 的容量和哈希函数,以充分利用哈希表的优势。同时,当 Map 中的键值对类型为复杂类型(如结构体)时,需要重载对应的哈希函数和比较函数,以确保哈希表的正确性。

32. Go Ma p 如何查找?

Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。

// 不带 comma 用法
value := m["name"]
fmt.Printf("value:%s", value)// 带 comma 用法
value, ok := m["name"]
if ok {fmt.Printf("value:%s", value)
}

map 的查找通过生成汇编码可以知道,根据 key 的不同类型/返回参数,编译器会将查找函数用更具体的函数替换,以优化效率:

key 类型查找
uint32mapaccess1_fast32(t maptype, h hmap, key uint32) unsafe.Pointer
uint32mapaccess2_fast32(t maptype, h hmap, key uint32) (unsafe.Pointer, bool)
uint64mapaccess1_fast64(t maptype, h hmap, key uint64) unsafe.Pointer
uint64mapaccess2_fast64(t maptype, h hmap, key uint64) (unsafe.Pointer, bool)
stringmapaccess1_faststr(t maptype, h hmap, ky string) unsafe.Pointer
stringmapaccess2_faststr(t maptype, h hmap, ky string) (unsafe.Pointer, bool)

查找流程

  

 1. 写保护监测

函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic,这也说明了 map 不是线程安全的。

if h.flags&hashWriting != 0 {throw("concurrent map read and map write")
}

2. 计 算 hash 值

hash := t.hasher(key, uintptr(h.hash0))

key 经过哈希函数计算后,得到的哈希值如下(主流 64 位机下共 64 个 bit 位),不同类型的 key 会有不同的 hash 函数。

 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010

3. 找 到 hash 对应的 bucket

bucket 定位:哈希值的低 B 个 bit 位,用来定位 key 所存放的 bucket。

如果当前正在扩容中,并且定位到的旧 bucket 数据还未完成迁移,则使用旧的 bucket(扩容前的 bucket)。

hash := t.hasher(key, uintptr(h.hash0))
// 桶的个数m-1,即 1<<B-1,B=5时,则有0~31号桶
m := bucketMask(h.B)
// 计算哈希值对应的bucket
// t.bucketsize为一个bmap的大小,通过对哈希值和桶个数取模得到桶编号,通过对桶编号和buckets起始地址进行运算,获取哈希值对应的bucket
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 是否在扩容
if c := h.oldbuckets; c != nil {// 桶个数已经发生增长一倍,则旧bucket的桶个数为当前桶个数的一半if !h.sameSizeGrow() {// There used to be half as many buckets; mask down one more power of two.m >>= 1}// 计算哈希值对应的旧bucketoldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))// 如果旧bucket的数据没有完成迁移,则使用旧bucket查找if !evacuated(oldb) {b = oldb}
}

4. 遍 历 bucket 查找

tophash 值定位:哈希值的高 8 个 bit 位,用来快速判断 key 是否已在当前 bucket 中(如果不在的话,需要去 bucket 的 overflow 中查找)。

用步骤 2 中的 hash 值,得到高 8 个 bit 位,也就是 10010111,转化为十进制,也就是 151

top := tophash(hash)
func tophash(hash uintptr) uint8 {top := uint8(hash >> (goarch.PtrSize*8 - 8))if top < minTopHash {top += minTopHash}return top
}

上面函数中 hash 是 64 位的,sys.PtrSize 值是 8,所以 top := uint8(hash >> (sys.PtrSize*8 - 8)) 等效 top = uint8(hash >> 56),最后 top 取出来的值就是 hash 的高 8 位值。

在 bucket 及 bucket 的 overflow 中寻找 tophash 值(HOB hash)为 151* 的 槽位,即为 key 所在位置,找到了空槽位或者 2 号槽位,这样整个查找过程就结束了,其中找到空槽位代表没找到。

for ; b != nil; b = b.overflow(t) {for i := uintptr(0); i < bucketCnt; i++ {if b.tophash[i] != top {// 未被使用的槽位,插入if b.tophash[i] == emptyRest {break bucketloop}continue}// 找到tophash值对应的的keyk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))if t.key.equal(key, k) {e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))return e}}}

  

5. 返回 key 对应的指针

如果通过上面的步骤找到了 key 对应的槽位下标 i,我们再详细分析下 key/value 值是如何获取的:

// keys的偏移量
dataOffset = unsafe.Offsetof(struct{b bmapv int64
}{}.v)// 一个bucket的元素个数
bucketCnt = 8// key 定位公式
k :=add(unsafe.Pointer(b),dataOffset+i*uintptr(t.keysize))// value 定位公式
v:= add(unsafe.Pointer(b),dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))

bucket 里 keys 的起始地址就是 unsafe.Pointer(b)+dataOffset。

第 i 个下标 key 的地址就要在此基础上跨过 i 个 key 的大小;

而我们又知道,value 的地址是在所有 key 之后,因此第 i 个下标 value 的地址还需要加上所有 key 的偏移。

33. Go Map 冲突的解决方式?

比较常用的 Hash 冲突解决方案有链地址法和开放寻址法:

1. 链地址法

当哈希冲突发生时,创建新单元,并将新单元添加到冲突单元所在链表的尾部。

2. 开放寻址法

当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。开放寻址法需要的表长度要大于等于所需要存放的元素数量

开放寻址法有多种方式:线性探测法、平方探测法、随机探测法和双重哈希法。这里以线性探测法来帮助读者理解开放寻址法思想。

线性探测法

Hash(key) 表示关键字 key 的哈希值, 表示哈希表的槽位数(哈希表的大小)。

线性探测法则可以表示为:

如果 Hash(x) % M 已经有数据,则尝试 (Hash(x) + 1) % M ;

如果 (Hash(x) + 1) % M 也有数据了,则尝试 (Hash(x) + 2) % M ;

如果 (Hash(x) + 2) % M 也有数据了,则尝试 (Hash(x) + 3) % M ;

两种解决方案比较

对于链地址法,基于数组 + 链表进行存储,链表节点可以在需要时再创建,不必像开放寻址法那样事先申请好足够内存,因此链地址法对于内存的利用率会比开方寻址法高。链地址法对装载因子的容忍度会更高,并且适合存储大对象、大数据量的哈希表。而且相较于开放寻址法,它更加灵活,支持更多的优化策略,比如可采用红黑树代替链表。但是链地址法需要额外的空间来存储指针。

对于开放寻址法,它只有数组一种数据结构就可完成存储,继承了数组的优点,对 CPU 缓存友好,易于序列化操作。但是它对内存的利用率不如链地址法,且发生冲突时代价更高。当数据量明确、装载因子小,适合采用开放寻址法。

总结

在发生哈希冲突时,Python 中 dict 采用的开放寻址法,Java 的 HashMap 采用的是链地址法,而 Go map 也采用链地址法解决冲突,具体就是插入 key 到 map 中时,当 key 定位的桶填满 8 个元素后(这里的单元就是桶,不是元素),将会创建一个溢出桶,并且将溢出桶插入当前桶所在链表尾部。

if inserti == nil {// all current buckets are full, allocate a new one.newb := h.newoverflow(t, b)// 创建一个新的溢出桶inserti = &newb.tophash[0]insertk = add(unsafe.Pointer(newb), dataOffset)elem = add(insertk, bucketCnt*uintptr(t.keysize))
}

34. Go Map 的负载因子为什么是 6.5?

什么是负载因子?

负载因子(load factor),用于衡量当前哈希表中空间占用率的核心指标,也就是每个 bucket 桶存储的平均元素个数。

负载因子 = 哈希表存储的元素个数/桶个数

另外负载因子与扩容、迁移等重新散列(rehash)行为有直接关系:

  • 在程序运行时,会不断地进行插入、删除等,会导致 bucket 不均,内存利用率低,需要迁移。

  • 在程序运行时,出现负载因子过大,需要做扩容,解决 bucket 过大的问题。

负载因子是哈希表中的一个重要指标,在各种版本的哈希表实现中都有类似的东西,主要目的是为了平衡 buckets 的存储空间大小和查找元素时的性能高低

在接触各种哈希表时都可以关注一下,做不同的对比,看看各家的考量。

为什么是 6.5?

为什么 Go 语言中哈希表的负载因子是 6.5,为什么不是 8 ,也不是 1。这里面有可靠的数据支撑吗?

测试报告

实际上这是 Go 官方的经过认真的测试得出的数字,一起来看看官方的这份测试报告。

报告中共包含 4 个关键指标,如下:

loadFactor%overflowbytes/entryhitprobemissprobe
42.1320.7734
4.54.0517.33.254.5
56.8514.773.55
5.510.5512.943.755.5
615.2711.6746
6.520.910.794.256.5
727.1410.154.57
7.534.039.734.757.5
841.19.458

  • loadFactor:负载因子,也有叫装载因子。

  • %overflow:溢出率,有溢出 bukcet 的百分比。

  • bytes/entry:平均每对 key/value 的开销字节数.

  • hitprobe:查找一个存在的 key 时,要查找的平均个数。

  • missprobe:查找一个不存在的 key 时,要查找的平均个数。

选择数值

Go 官方发现:装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。反之,装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数。

根据这份测试结果和讨论,Go 官方取了一个相对适中的值,把 Go 中的 map 的负载因子硬编码为 6.5,这就是 6.5 的选择缘由。

这意味着在 Go 语言中,当 map存储的元素个数大于或等于 6.5 * 桶个数 时,就会触发扩容行为

35. Go Map 的底层实现原理

Go 中的 map 是一个指针,占用 8 个字节,指向 hmap 结构体。

源码包中 src/runtime/map.go 定义了 hmap 的数据结构:

hmap 包含若干个结构为 bmap 的数组,每个 bmap 底层都采用链表结构,bmap 通常叫其 bucket。

  

hmap 结构体

// A header for a Go map.
type hmap struct {count     int // 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。flags     uint8 // 状态标志(是否处于正在写入的状态等)B         uint8  // buckets(桶)的对数// 如果B=5,则buckets数组的长度 = 2^B=32,意味着有32个桶noverflow uint16 // 溢出桶的数量hash0     uint32 // 生成hash的随机数种子buckets    unsafe.Pointer // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。oldbuckets unsafe.Pointer // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2;非扩容状态下,它为nil。nevacuate  uintptr        // 表示扩容进度,小于此地址的buckets代表已搬迁完成。extra *mapextra // 存储溢出桶,这个字段是为了优化GC扫描而设计的,下面详细介绍}

bmap 结构体

bmap 就是我们常说的 “桶”,一个桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果的低 B 位是相同的,关于 key 的定位我们在 map 的查询中详细说明。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有 8 个位置)。

// A bucket for a Go map.
type bmap struct {tophash [bucketCnt]uint8        // len为8的数组// 用来快速定位key是否在这个bmap中// 一个桶最多8个槽位,如果key所在的tophash值在tophash中,则代表该key在这个桶中
}

上面 bmap 结构是静态结构,在编译过程中 runtime.bmap 会拓展成以下结构体:

type bmap struct{tophash [8]uint8keys [8]keytype // keytype 由编译器编译时候确定values [8]elemtype // elemtype 由编译器编译时候确定overflow uintptr // overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}

tophash 就是用于实现快速定位 key 的位置,在实现过程中会使用 key 的 hash 值的高 8 位作为 tophash 值,存放在 bmap 的 tophash 字段中。

tophash 字段不仅存储 key 哈希值的高 8 位,还会存储一些状态值,用来表明当前桶单元状态,这些状态值都是小于 minTopHash 的。

为了避免 key 哈希值的高 8 位值和这些状态值相等,产生混淆情况,所以当 key 哈希值高 8 位若小于 minTopHash 时候,自动将其值加上 minTopHash 作为该 key 的 tophash。桶单元的状态值如下:

emptyRest      = 0 // 表明此桶单元为空,且更高索引的单元也是空
emptyOne       = 1 // 表明此桶单元为空
evacuatedX     = 2 // 用于表示扩容迁移到新桶前半段区间
evacuatedY     = 3 // 用于表示扩容迁移到新桶后半段区间
evacuatedEmpty = 4 // 用于表示此单元已迁移
minTopHash     = 5 // key的tophash值与桶状态值分割线值,小于此值的一定代表着桶单元的状态,大于此值的一定是key对应的tophash值func tophash(hash uintptr) uint8 {top := uint8(hash >> (goarch.PtrSize*8 - 8))if top < minTopHash {top += minTopHash}return top
}

mapextra 结构体

当 map 的 key 和 value 都不是指针类型时候,bmap 将完全不包含指针,那么 gc 时候就不用扫描 bmap。bmap 指向溢出桶的字段 overflow 是 uintptr 类型,为了防止这些 overflow 桶被 gc 掉,所以需要 mapextra.overflow 将它保存起来。如果 bmap 的 overflow 是 *bmap 类型,那么 gc 扫描的是一个个拉链表,效率明显不如直接扫描一段内存 (hmap.mapextra.overflow)。

type mapextra struct {overflow    *[]*bmap// overflow 包含的是 hmap.buckets 的 overflow 的 bucketsoldoverflow *[]*bma// oldoverflow 包含扩容时 hmap.oldbuckets 的 overflow 的 bucketnextOverflow *bmap // 指向空闲的 overflow bucket 的指针
}

总结

bmap(bucket)内存数据结构可视化如下:

注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式,当 key 和 value 类型不一样的时候,key 和 value 占用字节大小不一样,使用 key/value 这种形式可能会因为内存对齐导致内存空间浪费,所以 Go 采用 key 和 value 分开存储的设计,更节省内存空间。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Python实战——轻松实现动态网页爬虫(附详细源码)
  • Python 如何创建和操作矩阵?
  • ffmpeg将视频转换成图片
  • 【全面介绍下Gitea,什么是Gitea?】
  • 敏捷产品经理实训:助力产品负责人掌握敏捷方法,提升产品开发效率
  • VB中如何操作数据库(使用ADO.NET连接和查询数据库)
  • 在VMware里面安装Linux安装教程
  • 实时数仓Hologres TPC-H及点查性能开箱测试
  • 电商电子面单API对接方法
  • 【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(十四)-租云服务器及配环境、docker基本命令
  • 一文速通GIT版本管理与分支控制
  • 数模打怪(八)之图论模型
  • Linux第六节课yum与vim
  • vue3中Cesium离线地图
  • 【未来餐饮】 配送设置
  • 2017 前端面试准备 - 收藏集 - 掘金
  • Android系统模拟器绘制实现概述
  • spark本地环境的搭建到运行第一个spark程序
  • Spring Cloud(3) - 服务治理: Spring Cloud Eureka
  • swift基础之_对象 实例方法 对象方法。
  • Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比...
  • vue-loader 源码解析系列之 selector
  • Zepto.js源码学习之二
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 工程优化暨babel升级小记
  • 精彩代码 vue.js
  • 你真的知道 == 和 equals 的区别吗?
  • 说说动画卡顿的解决方案
  • 微服务入门【系列视频课程】
  • 中文输入法与React文本输入框的问题与解决方案
  • mysql面试题分组并合并列
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • 数据可视化之下发图实践
  • #Linux(帮助手册)
  • (13)[Xamarin.Android] 不同分辨率下的图片使用概论
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (独孤九剑)--文件系统
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (二)JAVA使用POI操作excel
  • (附源码)springboot建达集团公司平台 毕业设计 141538
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (过滤器)Filter和(监听器)listener
  • (没学懂,待填坑)【动态规划】数位动态规划
  • (三)SvelteKit教程:layout 文件
  • (实战篇)如何缓存数据
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (四)图像的%2线性拉伸
  • (四十一)大数据实战——spark的yarn模式生产环境部署
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (转)shell中括号的特殊用法 linux if多条件判断
  • (转载)从 Java 代码到 Java 堆
  • .halo勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复