Go语言数据结构学习之map | 青训营笔记

45 阅读2分钟

这是我参与「第五届青训营 」笔记创作活动的第12天。

最近各大厂陆续开始收暑期实习的简历了,作为Go语言选手自然要好好学习一下Go语言相关知识。

go语言的map底层采用哈希实现,大致原理就是算出每个key的哈希值,用这个哈希值去找value。

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 // # live cells == size of map.  Must be first (used by len() builtin)
   flags     uint8
   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 seed

   buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
   oldbuckets unsafe.Pointer // 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
}

以上是map在源码中的定义,最重要的除了count之外,就是buckets了。 count字段存储了map中元素总数,由于len(map)会经常使用,所以专门维护这样一个字段。

而buckets就指向存数据的地方了。在程序实际运行过程中,它会指向存放了2^B个bmap链表的数组,每个key根据哈希值不同,落入这些bmap中。

type bmap struct {
  topbits  [8]uint8
  keys     [8]keytype
  values   [8]valuetype
  pad      uintptr
  overflow uintptr
}

显然,一个bmap中可以存8个键值对,topbits则是key哈希值的高8位。而要是一个bmap满了,就会像链表一样,在overflow字段中指向下一个新的bmap。

如果map中键值对的数量太多了;或者是删除了太多元素导致结构中空闲位置太多,就会触发扩容(对于后一种情况也可以叫重构)。

对于前一种情况,hmap.B会加一,即哈希空间扩大一倍来容纳更多元素;对于后一种情况,B不变,只需要让各个元素紧凑排列即可。

需要扩容时,hmap.oldbuckets中存放原来的结构。扩容的过程是渐进的,每次对map进行查询或复制时都会将最多两个bucket从oldbuckets搬到buckets。

很显然,由于扩容的存在,每个键值对在map中具体存放位置是不确定的,不像c++stl的map,可以按照大小顺序遍历(人家用红黑树存的)。go语言为了提醒大家这一点,遍历时故意随机进行。