go语言中的map是一种内建引用类型

map存储时key不可重复,无顺序,排序的话可以将key排序,然后取出对应value.只有可以比较的类型才可以作key,value则无限制.

go中的map采用的是哈希map

给定key后,会通过哈希算法计算一个哈希值,低B位(这里是大写的B,2^B表示当前map中bucket的数量)代表的是存在map中的哪一个bucket,高8位则是了存在bucket中的一个uint[8]数组中,高8位所在的数组indec可用来寻找对应key的index.

map可抽象为bucket结构体组成的结构体,bucket数量一开始是固定的,后期不够用之后会进行扩容,bucket内部含有数组,bucket内部数组存储的即为key和value,为8组数据,存储方式为key0,key1,key2…value0,value1,value2…形式,而不是key和value顺次存储,这样是为了防止key和vaule长度不一致时需要额外padding.key和value存储在同一个数组中.

1.map的结构:

type hmap struct {
    count     int // # 元素个数
    flags     uint8
    B         uint8  // 说明包含2^B个bucket
    noverflow uint16 // 溢出的bucket的个数
    hash0     uint32 // hash种子
    buckets    unsafe.Pointer // buckets的数组指针
    oldbuckets unsafe.Pointer // 结构扩容的时候用于复制的buckets数组
    nevacuate  uintptr        // 搬迁进度(已经搬迁的buckets数量)
    extra *mapextra
    }

bucket的数据结构

type bmap struct {
    // tophash generally contains the top byte of the hash value
    // for each key in this bucket. If tophash[0] < minTopHash,
    // tophash[0] is a bucket evacuation state instead.
    tophash [bucketCnt]uint8
    // Followed by bucketCnt keys and then bucketCnt values.
    // NOTE: packing all the keys together and then all the values together makes the
    // code a bit more complicated than alternating key/value/key/value/... but it allows
    // us to eliminate padding which would be needed for, e.g., map[int64]int8.
    // Followed by an overflow pointer.
    }

go语言map的使用方法 go中map存储_数组

 

图中len 就是当前map的元素个数,也就是len()返回的值。也是结构体中hmap.count的值。bucket array是指数组指针,指向bucket数组。hash seed 哈希种子。overflow指向下一个bucket。

map的底层主要是由三个结构构成:

  1. hmap --- map的最外层的数据结构,包括了map的各种基础信息、如大小、bucket,一个大的结构体。
  2. mapextra --- 记录map的额外信息,hmap结构体里的extra指针指向的结构,例如overflow bucket。
  3. bmap --- 代表bucket,每一个bucket最多放8个kv,最后由一个overflow字段指向下一个bmap,注意key、value、overflow字段都不显示定义,而是通过maptype计算偏移获取的。

  mapextra的结构如下

type mapextra struct {

// If both key and value do not contain pointers and are inline, then we mark bucket
    // type as containing no pointers. This avoids scanning such maps.
    // However, bmap.overflow is a pointer. In order to keep overflow buckets
    // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
    // overflow and oldoverflow are only used if key and value do not contain pointers.
    // overflow contains overflow buckets for hmap.buckets.
    // oldoverflow contains overflow buckets for hmap.oldbuckets.
    // The indirection allows to store a pointer to the slice in hiter.
     overflow    *[]*bmap
     oldoverflow *[]*bmap
 
    // nextOverflow holds a pointer to a free overflow bucket.
    nextOverflow *bmap
}

其中hmap.extra.nextOverflow指向的是预分配的overflow bucket,预分配的用完了那么值就变成nil。

  bmap的详细结构如下

go语言map的使用方法 go中map存储_数组_02

 

在map中出现哈希冲突时,首先以bmap为最小粒度挂载,一个bmap累积8个kv之后,就会申请一个新的bmap(overflow bucket)挂在这个bmap的后面形成链表,优先用预分配的overflow bucket,如果预分配的用完了,那么就malloc一个挂上去。这样减少对象数量,减轻管理内存的负担,利于gc。注意golang的map不会shrink,内存只会越用越多,overflow bucket中的key全删了也不会释放。

  bmap中所有key存在一块,所有value存在一块,这样做方便内存对齐。当key大于128字节时,bucket的key字段存储的会是指针,指向key的实际内容;value也是一样。

  hash值的高8位存储在bucket中的tophash字段。每个桶最多放8个kv对,所以tophash类型是数组[8]uint8。把高八位存储起来,这样不用完整比较key就能过滤掉不符合的key,加快查询速度。实际上当hash值的高八位小于常量minTopHash时,会加上minTopHash,区间[0, minTophash)的值用于特殊标记。查找key时,计算hash值,用hash值的高八位在tophash中查找,有tophash相等的,再去比较key值是否相同。

type typeAlg struct {

// function for hashing objects of this type
    // (ptr to object, seed) -> hash
    hash func(unsafe.Pointer, uintptr) uintptr
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(unsafe.Pointer, unsafe.Pointer) bool

    // tophash calculates the tophash value for hash.
    func tophash(hash uintptr) uint8 {
     top := uint8(hash >> (sys.PtrSize*8 - 8))
    if top < minTopHash {
       top += minTopHash
    }
    return top
}
golang为每个类型定义了类型描述器_type,并实现了hashable类型的_type.alg.hash和_type.alg.equal,以支持map的范型,定义了这类key用什么hash函数、bucket的大小、怎么比较之类的,通过这个变量来实现范型。

2. map的并发安全性

  map的并发操作不是安全的。并发起两个goroutine,分别对map进行数据的增加:

sync.Map的原理:sync.Map里头有两个map一个是专门用于读的read map,另一个是才是提供读写的dirty map;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。
特点:官方出品,通过空间换时间的方式,读写分离;不适用于大量写的场景,会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。适用场景:大量读,少量写。

  解决方案3:分段锁

  这也是数据库常用的方法,分段锁每一个读写锁保护一段区间。sync.Map其实也是相当于表级锁,只不过多读写分了两个map,本质还是一样的。

  优化方向:将锁的粒度尽可能降低来提高运行速度。思路:对一个大map进行hash,其内部是n个小map,根据key来来hash确定在具体的那个小map中,这样加锁的粒度就变成1/n了。例如

3. map的GC内存回收

  golang里的map是只增不减的一种数组结构,他只会在删除的时候进行打标记说明该内存空间已经empty了,不会回收。

可以看到delete是不会真正的把map释放的,所以要回收map还是需要设为nil
地址