go-map

  1. map
    1. 底层结构
    2. 扩容
    3. map的顺序打印

map

底层结构

底层hash表,map实际就是一个指针,指向hmap结构体

type hmap struct {
  count     int              // 存储的键值对数目
  flags     uint8            // 状态标志(是否处于正在写入的状态等)
  B         uint8            // 桶的数目 2^B
  noverflow uint16           // 使用的溢出桶的数量
  hash0     uint32           // 生成hash的随机数种子

  buckets    unsafe.Pointer  // bucket数组指针,数组的大小为2^B(桶)
  oldbuckets unsafe.Pointer  // 扩容阶段用于记录旧桶用到的那些溢出桶的地址
  nevacuate  uintptr         // 记录渐进式扩容阶段下一个要迁移的旧桶编号
  extra *mapextra            // 指向mapextra结构体里边记录的都是溢出桶相关的信息
}
// buckets则是指向hash表节点bmap即bucket的指针,go中一个桶最多装8个key
type bmap struct {
    tophash [bucketCnt]uint8        
    // len为8的数组,用来快速定位key是否在这个bmap中
    // 一个桶最多8个槽位,如果key所在的tophash值在tophash中,则代表该key在这个桶中。hash 值低8位用来定位 bucket,高8位定位 tophash。
}

当编译过后,runtime.bmap会扩展成以下结构

type bmap struct{
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr        // 内存对齐使用,可能不需要
    overflow uintptr        // 当bucket 的8个key 存满了之后
    // overflow 指向下一个溢出桶 bmap,
    // overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}

扩容

采用渐进式扩容,原有的key并不会一次性搬迁完毕,每次最多只会搬迁2个bucket,只有在插入或修改,删除key的时候,都会尝试进行搬迁buckets的工作。

  • 翻倍扩容
    当负载因子达到6.5的时候进行翻倍扩容。
    迁移过程中使用与运算发hash&(m-1),把旧桶迁移到新桶上,用旧桶的hash值跟扩容后的桶的个数m-1的值相与。
  • 等量扩容
    没有超过负载因子,使用溢出桶过多,触发等量扩容。通常这种情况发生在很多键值对被删除的情况,造成overflow的bucket数量增加,但负载因子不高。当重新迁移后,会重新排列,使得排列更紧凑。

map的顺序打印

乱序的原因:

  • 不是从固定的0号bucket开始的,是随机的
  • 扩容会发生key的搬迁
    因为golang中map访问的时候每次是乱序的,怎么设计一个有序的。注意map不是线程安全的,sync.map是线程安全的。
// 设计一个map顺序打印
// 使用切片(slice)保存键的顺序:为了保持插入键的顺序,我们可以使用一个切片保存 map 中的键。在写入 map 时,将键按照插入顺序放入该切片中,这样在读取时就可以按照插入顺序读取。
//
// 使用 sync.RWMutex 实现并发安全:Go 的 map 本身不是线程安全的,若在多个 goroutine 中读写,需要加锁。我们可以使用 sync.RWMutex 来实现读写锁,保证顺序读写时的并发安全。
type OrderedMap struct {
    data  map[string]interface{}
    keys  []string
    mutex sync.RWMutex
}

// NewOrderedMap 创建一个有序的 map
func NewOrderedMap() *OrderedMap {
    return &OrderedMap{
        data: make(map[string]interface{}),
        keys: make([]string, 0),
    }
}

// Set 插入键值对,保持顺序
func (om *OrderedMap) Set(key string, value interface{}) {
    om.mutex.Lock()
    defer om.mutex.Unlock()

    // 检查是否是新键
    if _, exists := om.data[key]; !exists {
        om.keys = append(om.keys, key)
    }
    om.data[key] = value
}

// Get 获取键的值
func (om *OrderedMap) Get(key string) (interface{}, bool) {
    om.mutex.RLock()
    defer om.mutex.RUnlock()

    value, exists := om.data[key]
    return value, exists
}

// Delete 删除键
func (om *OrderedMap) Delete(key string) {
    om.mutex.Lock()
    defer om.mutex.Unlock()

    // 删除数据
    if _, exists := om.data[key]; exists {
        delete(om.data, key)
        // 删除顺序中的键
        for i, k := range om.keys {
            if k == key {
                om.keys = append(om.keys[:i], om.keys[i+1:]...)
                break
            }
        }
    }
}

// Keys 返回所有键的顺序
func (om *OrderedMap) Keys() []string {
    om.mutex.RLock()
    defer om.mutex.RUnlock()

    // 返回键的副本,防止外部修改
    return append([]string{}, om.keys...)
}

// Iterate 顺序遍历所有键值对
func (om *OrderedMap) Iterate() {
    om.mutex.RLock()
    defer om.mutex.RUnlock()

    for _, key := range om.keys {
        fmt.Printf("Key: %s, Value: %v\n", key, om.data[key])
    }
}

func main() {
    // 创建有序 map
    orderedMap := NewOrderedMap()

    // 顺序写入
    orderedMap.Set("a", 1)
    orderedMap.Set("b", 2)
    orderedMap.Set("c", 3)

    // 顺序读取
    orderedMap.Iterate()

    // 获取单个值
    if val, ok := orderedMap.Get("b"); ok {
        fmt.Printf("Get b: %v\n", val)
    }

    // 删除一个键
    orderedMap.Delete("b")

    // 顺序读取
    orderedMap.Iterate()

    // 打印所有键
    fmt.Println("Keys:", orderedMap.Keys())
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。