go数据类型-map

这篇具有很好参考价值的文章主要介绍了go数据类型-map。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

go的map在面试时候经常会被问到。

最近看到群里有个被问到为什么map的每个桶中只装8个元素?

map 的结构

go数据类型-map

注:解决hash冲突还有一些别的方案:开放地址法 (往目标地址后面放)、再哈希法(再次hash)

底层定义

// A header for a Go map.
type hmap struct {
   // 个数 size of map,当使用len方法,返回就是这个值
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
    // 桶的以2为底的对数,后面在查找和扩容都用到这个值
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	// 溢出桶的数量 这里讲了 approximate 不是精准的
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    // 哈希的种子,在进行哈希运算算法是要用到
	hash0     uint32 // hash seed
    // 桶的数组,是 2^B个数,和B的定义对上了
	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    // 扩容时用于保存之前 buckets 的字段
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    // 指示扩容进度,小于此地址的 buckets 迁移完成
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
	extra *mapextra // optional fields
}

跟进看 buckets的结构:

bucketCnt  = abi.MapBucketCount =8 
// A bucket for a Go map.
type bmap struct {
	tophash [bucketCnt]uint8
  // Followed by bucketCnt keys and then bucketCnt elems.
  // Followed by an overflow pointer.
}

每个桶 定义了 有8个哈希值的容量。 这里就解释了为什么一个桶只能放八个元素。

至于元素的存储,在这里没有定义,主要是不能写死类型。

但是在编译期间,会把要存储的key 和value写进来;
最后还跟了一个 溢出指针。

整体的结构是这样:

go数据类型-map

bmap的数量根据B确定,如果B为2,那么bmap为4个,图中B为3。

每个bmap容量为8,超过8个就要用到溢出桶。 意味着每个桶最多只能存储8个元素。

map的创建

func main() {
	m := make(map[string]string, 10)
	fmt.Println(m)
}

看下是如何创建的:
通过看下汇编,发现最终调用了runtime.makemap()方法

go build -gcflags -S main.go
MOVL    $10, BX
XORL    CX, CX
PCDATA  $1, $0
NOP
CALL    runtime.makemap(SB)


func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
	

	// initialize Hmap 初始化 hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()
    // 对B进行赋值
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B
	if h.B != 0 {
		var nextOverflow *bmap
        // 这里创建一个bucket的数组,而且也创建了一些溢出桶,用extra 存储。
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}
	return h
}

这里可以返回去看下 bmap 的最后一个参数:

   extra *mapextra // optional fields
  type mapextra struct {

	overflow    *[]*bmap
	oldoverflow *[]*bmap
	nextOverflow *bmap
}

通过上面能看到,给 h.extra.nextOverflow = nextOverflow 存放了下一个可用的溢出桶的位置。

go数据类型-map

map的访问

1. 计算桶位

go数据类型-map
  1. 通过key + hash0种子 通过hash算法,等到一串32位的字符串。具体多少位与操作系统有关。
  1. 取哈希值的后B位。图中B为3,得到了 010 就是 2号桶。
  1. 得到的值 就是 桶的位置。

2. 访问进行匹配

go数据类型-map

这里有个 tophash,只存储了hash值的前8位。map里面挺多8的。

开始对比key为a的哈希值的前8位,如果找到了,则需要对比下key,因为前8位可能会有一样的

如果不匹配,则继续往后找,溢出桶,找到则返回值。

如果都找不到,则没有这个key。

map写入

go数据类型-map

基本和查找类似。

map扩容

当map 溢出桶太多时会导致严重的性能下降,就需要对map的桶进行扩容。

可能会触发扩容的情况: 后面会具体解释

装载因子超过 6.5(平均每个槽6.5个key)

使用了太多溢出桶(溢出桶超过了普通桶)

具体实现在 runtime.mapassign()中:

//截取其中的关键代码:
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
	hashGrow(t, h)
	goto again // Growing the table invalidates everything, so try again
}

  // overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

loadFactor:=count / (2^B) 即 装载因子 = map中元素的个数 / map中当前桶的个数

通过计算公式我们可以得知,装载因子是指当前map中,每个桶中的平均元素个数。

如果没有溢出桶,那么一个桶中最多有8个元素,当平均每个桶中的数据超过了6.5个,那就意味着当前容量要不足了,发生扩容。

  func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
	// If the threshold is too low, we do extraneous work.
	// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
	// "too many" means (approximately) as many overflow buckets as regular buckets.
	// See incrnoverflow for more details.
	if B > 15 {
		B = 15
	}
	// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
	return noverflow >= uint16(1)<<(B&15)
}

当 B < 15 时,如果overflow的bucket数量超过 2^B。
当 B >= 15 时,overflow的bucket数量超过 2^15。

map的扩容的类型

  1. 等量扩容:数据不多但是溢出桶太多了 (整理)
  2. 翻倍扩容:数据太多了

第一步:

创建一组新桶

oldbuckets 指向原有的桶数组

buckets 指向新的桶数组

map标记为扩容状态

实现源码:

func hashGrow(t *maptype, h *hmap) {
	
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) {
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
    //  新建桶
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	//更改B的值
	h.B += bigger
   // 更改map状态
	h.flags = flags
    // oldbuckets 指向原来的
	h.oldbuckets = oldbuckets
    // buckets 指向新桶
	h.buckets = newbuckets
	h.nevacuate = 0
	h.noverflow = 0
    
    // 赋值新的溢出桶
	if h.extra != nil && h.extra.overflow != nil {
		if h.extra.oldoverflow != nil {
			throw("oldoverflow is not nil")
		}
		h.extra.oldoverflow = h.extra.overflow
		h.extra.overflow = nil
	}
	if nextOverflow != nil {
		if h.extra == nil {
			h.extra = new(mapextra)
		}
		h.extra.nextOverflow = nextOverflow
	}
}

步骤2

将所有的数据从旧桶驱逐到新桶
采用渐进式驱逐
每次操作一个旧桶时,将旧桶数据驱逐到新桶
读取时不进行驱逐,只判断读取新桶还是旧桶
go数据类型-map

例如图中:原本的2号桶中的数据,要么去新的2号010,要么去新的6号桶。110

这部分的代码也在map.go

在 mapassign中有这个逻辑:
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {

    if h.growing() {
    		growWork(t, h, bucket)
    	} 
}

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 具体的逻辑就在这个 evacuate中实现的。
	evacuate(t, h, bucket&h.oldbucketmask())

	// evacuate one more oldbucket to make progress on growing
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

步骤3

  所有的旧桶驱逐完成后
  oldbuckets回收

后续

因为有涉及到扩容的问题,所以在并发时候,map可能会出现数据错误的问题。

例如 协程A在读1号桶的数据,同时协程B也过来,将1号桶的数据进行了驱逐。导致协程A拿不到收据。

解决办法1、在读写map时候加锁,但这会导致map性能不好。

解决办法2、使用sync.map进行替代,下篇会讲 如何规避map并发不安全的原理。文章来源地址https://www.toymoban.com/news/detail-747826.html

到了这里,关于go数据类型-map的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入探讨 Go 语言中的 Map 类型

    Go 语言中的 map 类型是一种非常强大且常用的数据结构,它提供了一种键值对的映射关系。本篇博客将深入讨论 Go 中的 map 类型,包括其基本用法、特性、以及一些最佳实践。 1. 声明和初始化 在 Go 中,你可以使用 make 函数来创建一个空的 map 。 map 的键和值可以是任意数据类

    2024年01月17日
    浏览(43)
  • 【Go面试向】实现map稳定的有序遍历的方式

    大家好 我是寸铁👊 总结了一篇实现map稳定的有序遍历的方式探讨的文章✨ 喜欢的小伙伴可以点点关注 💝 你对 map 了解多少?如果要实现第一个稳定的有序遍历有哪些方式? 你对 map 了解多少? 我对map有一定的了解。 map 是Go中的一种集合类型,用于存储键值对。在map 中,

    2024年01月22日
    浏览(43)
  • Flutter面试中常问到的问题

    以下是本人总结的一些可能会在Flutter面试中问到的问题,分享出来,帮助大家找工作时候使用; 一直在更新,一直在精简! 主要包括概念性问题和技术性问题: 概念性问题: 1. Flutter是什么?为什么选择Flutter? Flutter是一个由谷歌开发的开源UI框架,可以用于构建高性能、

    2024年02月02日
    浏览(40)
  • freeRTOS面试会问到的问题。

    1 移植到哪些平台,讲讲移植过程,占用哪些硬件资源 。 可以移植到多种平台,包括单片机、嵌入式处理器、微处理器等等。     1.2移植过程: 选择对应目标处理器架构的FreeRTOS版本。 安装相应的工具链。 对FreeRTOS进行配置。 实现FreeRTOS底层函数。 搭建FreeRTOS应用程序,实现

    2024年02月12日
    浏览(41)
  • 【面试】Java面试频繁问到的题最新整理(附答案)

    封装 :对象只需要 选择性的对外公开一些属性和行为 。 继承 :子对象 可以继承父对象的属性和行为 ,并且可以在其之上进行修改以适合更特殊的场景需求。 多态 : 允许不同类的对象对同一消息做出响应 。 数据类型 占用字节 byte 1 short 2 int 4 long 8 float 4 double 8 char 2 boo

    2024年02月07日
    浏览(46)
  • 【Go面试向】rune和byte类型的认识与使用

    大家好 我是寸铁👊 总结了一篇rune和byte类型的认识与使用的文章✨ 喜欢的小伙伴可以点点关注 💝 byte ,占用 1 个字节,共8个比特位,所以它实际上和 uint8 没什么本质区别,它表示的是一个 ASCII 码字符。 rune ,占用 4 个字节,共 32 个比特位,所以它实际上和 int32 没什么本质

    2024年01月19日
    浏览(33)
  • 面试被问到:测试计划和测试方案有什么区别?

    面试的时候,很多小伙伴都被面试官问过这个问题 “测试计划和测试方案有什么区别”? 到底有什么区别呢?我们先好好了解下这两个文档。 1、测试计划是什么? 测试计划是组织管理层面的文件,从组织管理的角度对一次测试活动进行规划。对测试全过程的测试范围、组

    2023年04月14日
    浏览(80)
  • 面试中常被问到sql优化几种方案

    目录 一、索引优化 二、合理的查询设计 三、分页优化: 四、内存管理和缓存: 五、合理使用批量操作: 六、使用连接池: 七、分区表: 八、避免使用SELECT : 九、数据库升级和优化器统计信息: 十、避免不必要的约束和触发器: 十一、使用EXPLAIN分析查询计划: 十二、

    2024年02月10日
    浏览(68)
  • Lucene全文检索,阿里面试100%会问到的JVM

    全文检索大体分两个过程,索引创建(Indexing):将现实世界中所有的结构化和非结构化数据提取信息,搜索索引(Search):通过用户的查询请求搜索创建的索引,然后返回查询结果的过程。 Lucene实现全文检索的也同样需要这两个过程,其具体实现如下: 创建索引: 获得文档,表

    2024年03月20日
    浏览(51)
  • 大厂HR经常会问到的Java线程池面试题

    一、什么是线程池         线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。 二、为什么要使用线程池         因为在项目开发过程中频繁的开启线程或者停止线程,线程需要重新被CPU从就绪状态调度到运行状态,需要发生CPU的上下

    2024年02月14日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包