Golang Map介绍

什么是Map

map又称字典,是一种常用的数据结构,核心特征包含:

  • 存储基于key-value键值对模式,key不能重复,value可以重复;
  • 增、删、改、查时间复杂度为O(1);

在go语言中,map底层采用hash表,用变种拉链法拉解决hash冲突;

哈希冲突

hash表的原理是将多个kay-value对散列的存储在buckets中,buckets可以理解为一个连续的数组,所以给定一个key/value键值对,将其存入到map中必须要经历两个步骤:

  1. 计算hash值:hash = hashFunc(key)
  2. 计算索引位置:index = hash % len(buckets)

在计算hash值时会用到hash函数:

Hash 函数是一种将任意长度的输入映射为固定长度输出的函数。这个输出通常是一个较短的、固定大小的值,称为 hash 值或 hash 码。由于 hash 函数的输出长度是固定的,而输入长度是可变的,根据鸽巢原理(Pigeonhole Principle),当输入集合足够大时,必然存在至少两个输入映射到同一个输出上。

当输入两个不同key得到两个相同的hash值,那么这两个key对应的索引也必然相同,那么这个时候该如何去存储这两个键值对?这就是hash冲突问题。

解决hash冲突一般有两种方式:拉链法和开发地址法。

拉链法

拉链法是一种最常见的解决hash冲突的方法,很多语言(Java)都是基于该方法解决hash冲突。拉链法的主要实现是底层不直接使用连续数组来直接存储元素,而是使用数组加链表的组合方式,数组里存储的其实是一个指针,指向一个链表。当出现两个key的哈希值相同的情况,就将数据连起来,如果对应桶中没有发生hash冲突链表上就只有一个节点。拉链法的缺点就是当冲突严重时,链表会很长,访问性能会下降为O(n),解决方法就当链表长度超过一定阈值时将链表转换为其他数据结构如红黑树O(log n);

 开放地址法

开发地址法是将具体的数据存储在数组桶中,在要插入新元素时,先根据hash函数计算出hash值,在计算索引值,如果发现冲突了,就继续往后探测,直到找到未使用的数据曹为止。

 golang 解决map中的hash冲突时,结和了拉链法和开发地址发两种思路:

  • (1)桶数组中的每个桶,严格意义上是一个单向桶链表,以桶为节点进行串联;
  • (2)每个桶固定可以存放 8 个 key-value 对;
  • (3)当 key 命中一个桶时,首先根据开放寻址法,在桶的 8 个位置中寻找空位进行插入;
  • (4)倘若桶的 8 个位置都已被占满,则基于桶的溢出桶指针,找到下一个桶,重复第(3)步;
  • (5)倘若遍历到链表尾部,仍未找到空位,则基于拉链法,在桶链表尾部续接新桶,并插入 key-value 对.

 基本用法

初始化

map1 := map[int]int {  //[key类型]value类型

1:2,
3:4,

}

//初始化连带赋值


map2 := make(map[int]int,2)

//通过 make 关键字进行初始化,同时指定 map 预分配的容量.


map3 := make(map[int]int)

//通过 make 关键字进行初始化,不显式声明容量,因此默认容量 为 0.

map中的key的数据类型必须为可比较的类型,map,slice,func等类型数据不能作为map的key

  1. 切片(slice):切片在内存中的表示可能会改变,且两个包含相同元素的切片在使用==运算符进行比较时会产生编译错误,因此切片不能作为map的key。

  2. 映射(map):与切片类似,map也是不可比较的,因为map的内部结构可能会随着元素的添加或删除而改变。

  3. 函数(function):函数类型同样不可比较,因为函数在内存中的表示和状态可能复杂且多变,不支持简单的等值比较

访问

v1 := map1[10]


//第一种方式是直接读,倘若 key 存在,则获取到对应的 val,倘若 key 不存在或者 map 未初始化,会返回 val 类型的零值作为兜底.


v2,ok := map1[10]

//第二种方式是读的同时添加一个 bool 类型的 flag 标识是否读取成功. 倘若 ok == false,说明读取失败, key 不存在,或者 map 未初始化.

map1[5] = 6

//不存在新增,存在覆盖
//写操作的语法如上. 须注意的一点是,倘若 map 未初始化,直接执行写操作会导致 panic

delete(map1,5)

//执行 delete 方法时,倘若 key 存在,则会从 map 中将对应的 key-value 对删除;倘若 key 不存在或 map 未初始化,则方法直接结束,不会产生显式提示.

遍历

for k,v := range myMap{
  //...
}

//基于 k,v 依次承接 map 中的 key-value 对;


for k := range myMap{
  // ...
}

//基于 k 依次承接 map 中的 key,不关注 val 的取值.


//需要注意的是,在执行 map 遍历操作时,获取的 key-value 对并没有一个固定的顺序,因此前后两次遍历顺序可能存在差异.

线程不安全

map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.

具体规则是:

  • (1)并发读没有问题;
  • (2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;
  • (3)读的时候发现其他 goroutine 在并发写,抛出 fatal error;
  • (4)写的时候发现其他 goroutine 在并发写,抛出 fatal error.

底层数据结构

type hmap struct {
    count     int    //map 中的 key-value 总数;
    flags     uint8  //map 状态标识,可以标识出 map 是否被 goroutine 并发读写;
    B         uint8  //桶数组长度的指数,桶数组长度为 2^B;
    noverflow uint16 //map 中溢出桶的数量;
    hash0     uint32 //hash 随机因子,生成 key 的 hash 值时会使用到;
    buckets    unsafe.Pointer //桶数组;
    oldbuckets unsafe.Pointer //扩容过程中老的桶数组;
    nevacuate  uintptr       //扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中;
    extra *mapextra //预申请的溢出桶.
}

初始化流程

func makemap(t *maptype, hint int, h *hmap) *hmap {
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
        hint = 0
    }


    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()


    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B


    if h.B != 0 {
        var nextOverflow *bmap
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
        if nextOverflow != nil {
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }


    return 

hint 为 map 拟分配的容量;在分配前,会提前对拟分配的内存大小进行判断,倘若超限,会将 hint 置为零;

mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
   hint = 0
}

通过 new 方法初始化 hmap;

if h == nil {
   h = new(hmap)
}

调用 fastrand,构造 hash 因子:hmap.hash0;

h.hash0 = fastrand()

大致上基于 log2(B) >= hint 的思路,计算桶数组的容量 B;

B := uint8(0)
for overLoadFactor(hint, B) {
    B++
}
h.B =

调用 makeBucketArray 方法,初始化桶数组 hmap.buckets;

var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, n)

倘若 map 容量较大,会提前申请一批溢出桶 hmap.extra.

if nextOverflow != nil {
   h.extra = new(mapextra)
   h.extra.nextOverflow = nextOverflow
}

 读流程

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    if h == nil || h.count == 0 {
        return unsafe.Pointer(&zeroVal[0])
    }
    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)))
    if c := h.oldbuckets; c != nil {
        if !h.sameSizeGrow() {
            m >>= 1
        }
        oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
        if !evacuated(oldb) {
            b = oldb
        }
    }
    top := tophash(hash)
bucketloop:
    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
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            if t.key.equal(key, k) {
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                    e = *((*unsafe.Pointer)(e))
                }
                return e
            }
        }
    }
    return unsafe.Pointer(&zeroVal[0])
}


func (h *hmap) sameSizeGrow() bool {
    return h.flags&sameSizeGrow != 0
}


func evacuated(b *bmap) bool {
    h := b.tophash[0]
    return h > emptyOne && h < minTopHash
}
  1. 倘若 map 未初始化,或此时存在 key-value 对数量为 0,直接返回零值;
  2. 倘若发现存在其他 goroutine 在写 map,直接抛出并发读写的 fatal error;
  3. 根据 key 取 hash 值;
  4. 根据 hash 值对桶数组取模,确定所在的桶;
  5. 沿着桶链表依次遍历各个桶内的 key-value 对;
  6. 命中相同的 key,则返回 value;倘若 key 不存在,则返回零值.

 写流程

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    if h == nil {
        panic(plainError("assignment to entry in nil map"))
    }
    if h.flags&hashWriting != 0 {
        fatal("concurrent map writes")
    }
    hash := t.hasher(key, uintptr(h.hash0))


    h.flags ^= hashWriting


    if h.buckets == nil {
        h.buckets = newobject(t.bucket) 
    }


again:
    bucket := hash & bucketMask(h.B)
    if h.growing() {
        growWork(t, h, bucket)
    }
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    top := tophash(hash)


    var inserti *uint8
    var insertk unsafe.Pointer
    var elem unsafe.Pointer
bucketloop:
    for {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if isEmpty(b.tophash[i]) && inserti == nil {
                    inserti = &b.tophash[i]
                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                }
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            if !t.key.equal(key, k) {
                continue
            }
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            goto done
        }
        ovf := b.overflow(t)
        if ovf == nil {
            break
        }
        b = ovf
    }


    if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
        hashGrow(t, h)
        goto again 
    }


    if inserti == nil {
        newb := h.newoverflow(t, b)
        inserti = &newb.tophash[0]
        insertk = add(unsafe.Pointer(newb), dataOffset)
        elem = add(insertk, bucketCnt*uintptr(t.keysize))
    }


    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    if t.indirectelem() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(elem) = vmem
    }
    typedmemmove(t.key, insertk, key)
    *inserti = top
    h.count++




done:
    if h.flags&hashWriting == 0 {
        fatal("concurrent map writes")
    }
    h.flags &^= hashWriting
    if t.indirectelem() {
        elem = *((*unsafe.Pointer)(elem))
    }
    retur
  1. 写操作时,倘若 map 未初始化,直接 panic;
  2. 倘若其他 goroutine 在进行写或删操作,抛出并发写 fatal error;
  3. 根据 key 取 hash 值;
  4. 通过异或位运算,将 map.flags 的第 3 个 bit 位置为 1,添加写标记;
  5. 倘若 map 的桶数组 buckets 未空,则对其进行初始化;
  6. 根据 hash 值对桶数组取模,确定所在的桶;
  7. 倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;
  8. 沿着桶链表依次遍历各个桶内的 key-value 对;
  9. 倘若命中相同的 key,则对 value 中进行更新;
  10. 倘若 key 不存在,则插入 key-value 对;
  11. 倘若发现 map 达成扩容条件,则会开启扩容模式.
  12. 再次校验是否有其他协程并发写,倘若有,则抛 fatal error.
  13. 将 hmap.flags 中的写标记抹去,然后退出方法.

 删除流程

unc mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
    if h == nil || h.count == 0 {
        return
    }
    if h.flags&hashWriting != 0 {
        fatal("concurrent map writes")
    }


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


    h.flags ^= hashWriting


    bucket := hash & bucketMask(h.B)
    if h.growing() {
        growWork(t, h, bucket)
    }
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    bOrig := b
    top := tophash(hash)
search:
    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 search
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            k2 := k
            if t.indirectkey() {
                k2 = *((*unsafe.Pointer)(k2))
            }
            if !t.key.equal(key, k2) {
                continue
            }
            // Only clear key if there are pointers in it.
            if t.indirectkey() {
                *(*unsafe.Pointer)(k) = nil
            } else if t.key.ptrdata != 0 {
                memclrHasPointers(k, t.key.size)
            }
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            if t.indirectelem() {
                *(*unsafe.Pointer)(e) = nil
            } else if t.elem.ptrdata != 0 {
                memclrHasPointers(e, t.elem.size)
            } else {
                memclrNoHeapPointers(e, t.elem.size)
            }
            b.tophash[i] = emptyOne
            if i == bucketCnt-1 {
                if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
                    goto notLast
                }
            } else {
                if b.tophash[i+1] != emptyRest {
                    goto notLast
                }
            }
            for {
                b.tophash[i] = emptyRest
                if i == 0 {
                    if b == bOrig {
                        break
                    }
                    c := b
                    for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
                    }
                    i = bucketCnt - 1
                } else {
                    i--
                }
                if b.tophash[i] != emptyOne {
                    break
                }
            }
        notLast:
            h.count--
            if h.count == 0 {
                h.hash0 = fastrand()
            }
            break search
        }
    }


    if h.flags&hashWriting == 0 {
        fatal("concurrent map writes")
    }
    h.flags &^= hashWritin

倘若 map 未初始化或者内部 key-value 对数量为 0,删除时不会报错,直接返回;

倘若存在其他 goroutine 在进行写或删操作,抛出并发写的 fatal error;

根据 key 取 hash 值;

根据 hash 值对桶数组取模,确定所在的桶;

倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;

沿着桶链表依次遍历各个桶内的 key-value 对;

倘若命中相同的 key,删除对应的 key-value 对;并将当前位置的 tophash 置为 emptyOne,表示为空;

倘若当前位置为末位,或者下一个位置的 tophash 为 emptyRest,则沿当前位置向前遍历,将毗邻的 emptyOne 统一更新为 emptyRest.

 遍历流程

 扩容流程

map 的扩容类型分为两类,一类叫做增量扩容,一类叫做等量扩容.

(1)增量扩容

表现:扩容后,桶数组的长度增长为原长度的 2 倍;

目的:降低每个桶中 key-value 对的数量,优化 map 操作的时间复杂度.

(2)等量扩容

表现:扩容后,桶数组的长度和之前保持一致;但是溢出桶的数量会下降.

目的:提高桶主体结构的数据填充率,减少溢出桶数量,避免发生内存泄漏.

何时扩容

(1)只有 map 的写流程可能开启扩容模式;

(2)写 map 新插入 key-value 对之前,会发起是否需要扩容的逻辑判断:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // ...
    
    if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
        hashGrow(t, h)
        goto again
    }


    // ...
}

根据 hmap 的 oldbuckets 是否空,可以判断 map 此前是否已开启扩容模式:

func (h *hmap) growing() bool {
    return h.oldbuckets != nil
}

倘若此前未进入扩容模式,且 map 中 key-value 对的数量超过 8 个,且大于桶数组长度的 6.5 倍,则进入增量扩容:

const(
   loadFactorNum = 13
   loadFactorDen = 2
   bucketCnt = 8
)


func overLoadFactor(count int, B uint8) bool {
    return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

倘若溢出桶的数量大于 2^B 个(即桶数组的长度;B 大于 15 时取15),则进入等量扩容:

func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
    if B > 15 {
        B = 15
    }
    return noverflow >= uint16(1)<<(B&15)
}

扩容元素迁移规则

在等量扩容中,新桶数组长度与原桶数组相同;

key-value 对在新桶数组和老桶数组的中的索引号保持一致;

在增量扩容中,新桶数组长度为原桶数组的两倍;

把新桶数组中桶号对应于老桶数组的区域称为 x 区域,新扩展的区域称为 y 区域.

实际上,一个 key 属于哪个桶,取决于其 hash 值对桶数组长度取模得到的结果,因此依赖于其低位的 hash 值结果.;

在增量扩容流程中,新桶数组的长度会扩展一位,假定 key 原本从属的桶号为 i,则在新桶数组中从属的桶号只可能是 i (x 区域)或者 i + 老桶数组长度(y 区域);

当 key 低位 hash 值向左扩展一位的 bit 位为 0,则应该迁往 x 区域的 i 位置;倘若该 bit 位为 1,应该迁往 y 区域对应的 i + 老桶数组长度的位置.

渐进式扩容

map 采用的是渐进扩容的方式,避免因为一次性的全量数据迁移引发性能抖动.

当每次触发写、删操作时,会为处于扩容流程中的 map 完成两组桶的数据迁移:

(1)一组桶是当前写、删操作所命中的桶;

(2)另一组桶是,当前未迁移的桶中,索引最小的那个桶.

func growWork(t *maptype, h *hmap, bucket uintptr) {
    // make sure we evacuate the oldbucket corresponding
    // to the bucket we're about to use
    evacuate(t, h, bucket&h.oldbucketmask())


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值