对 golang map 的认识


谈一谈对 golang map 的认识,map 是属于哈希结构数据类型,用于存储键值对,键值唯一,存储没有先后顺序。和切片类似,都是引用传值。

初始化及基本操作

map 一般情况下用 make 函数初始化, 也可以使用字面量直接初始化一些初值

m := make(map[string]int)
m["a"] = 2 // 若 'a' 不存在,新建键值对,若存在,则修改
fate := map[string]string{
    "fsn": "fate stay night",
    "fz": "fate zero",
    "fgo": "fate grand order",  // 每行结尾需要逗号,若与花括号写在同一行,则最后一个元素不需要
}
fmt.Println(fate["fsn"]) // 通过 key 访问 value, 元素不存在返回类型的零值
delete(fate, "fgo")      // 使用 delete 函数删除键值对,无论元素是否存在都会执行,且没有返回值
if val, ok := fate["fsn-ubw"]; ok {
    // 可以使用第二返回值 ok 检测键值对是否存在
    // 并且返回的 val 不能进行取地址操作,当 map 扩容后,该键值的地址会发生改变,
    // fmt.Println(&val) // 编译不会通过
}
for k, v := range fate {
    // 使用 range 对键值对进行遍历,每次返回的顺序是随机的,是因为要考虑到 map 扩容带来的内存变动,故意设定每次随机返回
    fmt.Println(k, v)
}

线程安全

map 在单个 goroutine 内是安全的,因为存取都是顺序操作。
当有多个 goroutine 同时对 map 进行读写操作时,会发生 panic,因为 map 内部有写状态检测标志 flag,当 flag==1 时,表示正在写操作,此时再有 goroutine 的写操作进来,就会直接抛出异常。

解决方法也非常简单,一种方法是将 map 放在结构体中,并添加锁 sync.RWMutex,当然这会减缓服务器的速度,所以为了效率,尽量确保多个 goroutine 对 map 都是读操作。

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}
// 读取操作
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
// 写操作
counter.Lock()
counter.m["some_key"]++
counter.Unlock()

另一种方法是使用 golang 官方的 sync 包中的 Map,可以实现安全读写操作。

底层结构

关于 map 的底层结构,我没有深入研究,有兴趣可以研读下面的文章:

这里简单讲一下扩容的过程

扩容

map 为了提高哈希查找速率,将键值对数据存储在多个容量只有 8 的桶内,也就是一桶内最多存储8对数据,桶之间通过链表连接。当 map 到达扩容条件时,创建新桶并进行数据迁移。

了解扩容前,需要知道一个概念,go 定义了一个变量 装载因子,如下所示

// 装载因子  =  元素个数 / 桶的数量
loadFactor := count / (2^B)

扩容有两个条件可以触发:

  1. 装载因子超过 6.5 时
  2. 溢出桶(overflow buckets)过多

溢出桶简单理解就是已经存满了的桶额外添加的桶,

针对不同的条件,有不同的扩容策略

  • 对于条件1,go 会添加新桶,容量翻倍,并将旧桶中的数据迁移到新桶中
  • 对于条件2,go 开辟新的内存区,容量和之前相等,并将之前旧桶中的数据全部迁移过去,通过 GC 机制释放之前的内存区,达到整理内存,提高访问效率的目的

扩容完成后就需要迁移数据,但是迁移并不是一次性完成的,而是在每次进行写操作之后会迁移部分数据,直到旧桶中的数据迁移完成


文章作者: MaZhuang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MaZhuang !
  目录