【Golang】Go Map的底层原理解析之构造方法、读流程、插入流程以及删除流程

本文将详细介绍 Go 语言中 map 的数据结构及其核心操作,包括构造、读取、插入和删除的流程。本文内容基于 Go 运行时源码,帮助你深入理解 Go map 的内部实现。

数据结构

hmap

Go map 的头部结构定义如下:

// A header for a Go map.
type hmap struct {
   
    // 元素个数,调用 len(map) 时,直接返回此值
	count     int
	flags     uint8
	// buckets 的对数 log_2,
	B         uint8
	// overflow 的 bucket 近似数
	noverflow uint16
	// 计算 key 的哈希的时候会传入哈希函数
	hash0     uint32
    // 指向 buckets 数组(bmap),大小为 2^B
    // 如果元素个数为0,就为 nil
	buckets    unsafe.Pointer
	// 等量扩容的时候,buckets 长度和 oldbuckets 相等
	// 双倍扩容的时候,buckets 长度会是 oldbuckets 的两倍
	// 指向老桶数组,扩容时用
	oldbuckets unsafe.Pointer
	// 指示扩容进度,小于此地址的 buckets 迁移完成
	nevacuate  uintptr
	extra *mapextra // 溢出桶
}

bmap

type bmap struct {
   
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}
  • 每个桶(bmap)存储最多 8 个键值对。经过哈希计算后,具有相同哈希高 8 位(称为 tophash)的键会被分配到同一桶中。桶内会根据 key 对应的哈希高 8 位选择存储槽位。

mapextra

mapextra 用于管理桶数组中使用的溢出桶,结构如下:

type mapextra struct {
   
	overflow    *[]*bmap // 供桶数组 buckets 使用的溢出桶;
	oldoverflow *[]*bmap // 扩容流程中,供老桶数组 oldBuckets 使用的溢出桶;
	nextOverflow *bmap // 下一个可用的溢出桶.
}
  • 一图胜千言
    在这里插入图片描述

构造方法

在创建 map 时,会根据预估的大小(hint)计算所需桶的数量,并预先分配桶数组与溢出桶(若需要)。
在这里插入图片描述

func makemap(t *maptype, hint int, h *hmap) *hmap {
   
  /*
    hint: 拟分配的map的大小;
    在分配前,会提前对拟分配的内存大小进行判断,倘若超限,会将 hint 置为零;
  */
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
   
        hint = 0
    }


    if h == nil {
   
        h = new(hmap)
    }
    // 计算hash因子
    h.hash0 = fastrand()


    B := uint8(0)
    // 计算B的大小
    for overLoadFactor(hint, B) {
   
        B++
    }
    h.B = B


    if h.B != 0 {
   
        var nextOverflow *bmap
        // 初始化桶数组
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
        // 如果map容量过大
        if nextOverflow != nil {
   
            // 申请一批溢出桶
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }

    return h
}

map的读流程

在读取操作时,首先判断 map 是否为空;接着通过传入的 key 计算哈希值,根据哈希值定位到对应的桶;如果 map 正处于扩容状态,还需要判断该桶是否已完成从旧 map 向新 map 的迁移。
读流程

  • 如何找到key对应的桶呢?比较重要的地方就是:如果此时map正在扩容,还需要判断这个key所在的桶是否完成了从“旧map”到“新map”的迁移。(有关map扩容的相关细节,我会专门写一篇博客介绍)
    在这里插入图片描述
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
   
    // 如果map为空 或者 map中没有元素
    if h == nil || h.count == 0 {
   
        return unsafe.Pointer(&zeroVal[0])
    }
    // 如果当前有其他goroutine正在对map进行写操作
    if h.flags&hashWriting != 0 {
   
        fatal("concurrent map read and map write")
    }
    hash := t.hasher(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    // 在桶数组中,找到对应的桶
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
  // 如果map正在扩容,就要判断:{key,val}所在的桶,是否完成了迁移(通过hmap中的nevacuate)
    if c := h.oldbuckets; c != nil {
   
        // 如果是增量扩容,老桶数组的长度是新桶数组长度的一半
        if !h.sameSizeGrow()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值