go map

163 阅读3分钟

为什么go map 不支持并发操作: 在实际情况下,map可能是某些已经同步的较大数据结构或计算的一部分。因此,要求所有map操作都互斥将减慢大多数程序的速度,而只会增加少数程序的安全性。 具体可以看官网原文链接:Frequently Asked Questions (FAQ) - go.dev (google.cn)

map数据结构:


type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // map元素数量 # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8  // map 的状态
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // 计算hash值 hash seed

	buckets    unsafe.Pointer // 指向当前map桶的指针 array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // 在扩容时,存储旧桶,当旧桶的数据迁移至buckets后。则清空。previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // 溢出桶  optional fields
}

图片.png

真正用来存储键值对数据的是桶,也就是 bucket,每个 bucket 中存储的是 Hash 值低 bit 位数值相同的元素,默认的元素个数为 BUCKETSIZE(值为 8,Go 1.17 版本中在 $GOROOT/src/cmd/compile/internal/reflectdata/reflect.go 中定义,与 runtime/map.go 中常量 bucketCnt 保持一致)。

当某个 bucket(比如 buckets[0]) 的 8 个空槽 slot)都填满了,且 map 尚未达到扩容的条件的情况下,运行时会建立 overflow bucket即溢出桶(而这个overflow bucket在扩容时也会用到)并将这个 overflow bucket 挂在上面 bucket(如 buckets[0])末尾的 overflow 指针上,这样两个 buckets 形成了一个链表结构,直到下一次 map 扩容之前,这个结构都会一直存在。

Go语言选择将key与value分开存储而不是以key/value/key/value的形式存储,是为了在字节对齐时压缩空间。

map初始化:

调用makemap函数

图片.png

makemap函数会计算出需要的桶的数量,即log2N,并调用makeBucketArray函数生成桶和溢出桶。如果初始化时生成了溢出桶,则会放置到map的extra字段里去。

makeBucketArray会为map申请内存,需要注意的是,只有map的数量大于24,才会在初始化时生成溢出桶。溢出桶的大小为2(b-4),其中,b为桶的大小。

map 扩容:

有两种原因会导致map扩容:

  • 1、map超过负载因子大小 当超过其大小后,map会进行扩容,增大到原来buckets 2倍的大小。
  • 2、溢出桶(即上文中提到的 overflow bucket)的数量过多 如果是因为 overflow bucket 过多导致的“扩容”,实际上运行时会新建一个和现有规模一样的 bucket 数组,然后在 assign 和 delete 时做排空和迁移。

负载因子=哈希表中的元素数量/桶的数量。 目前go中的负载因子为6.5。

扩容示意图: 原 bucket 数组会挂在 hmap 的 oldbuckets 指针下面,直到原 buckets 数组中所有数据都迁移到新数组后,原 buckets 数组才会被释放。 图片.png

注意: go不允许获取map中value的地址。因为map在自动扩容中,map中数据元素的value位置可能在这一过程发生变化。