优秀的编程知识分享平台

网站首页 > 技术文章 正文

go 语言中的 map 类型的不完全整理

nanyue 2024-10-01 13:10:02 技术文章 12 ℃

哈希表(key-value)映射几乎是每种开发语言都会内建提供的数据结构,go 语言中就内建了map类型。本文将讨论和整理一下 go 语言中关于 map 的使用。

go 内建的 map[k]v 类型

map 是一组相同类型(v)的无序的元素(element)集合,可以被另一组键类型(k)的键集合索引。未初始化的 map 值为 nil(零值),我们可以将其称为 nil map。

var m map[string]int
fmt.Println(m == nil)
fmt.Println(m)
// Output:
// true
// map[]

可以使用索引表达式来检索 nil map 不会 panic,而是会得到零值;但不能向 nil map 中添加任何元素,否则将会引发 panic。

var m map[string]int
fmt.Println(m["foo"]) // 0
m["foo"] = 1          // panic: assignment to entry in nil map

map 的 key 类型必须定义比较操作符==!=,即可以使用这两个比较操作符进行比较,因此,key 的类型就不能是funcmapslice。 如果 key 的类型是 interface(接口类型),则必须为动态的键值定义这些比较运算符,否则在运行时将会 panic。

// m2 := map[func()]int{} // 无法编译: incomparable map key type func()compilerIncomparableMapKey
m3 := map[interface{}]int{}
var a = func() {}
m3[a] = 3 // panic: runtime error: hash of unhashable type func()

使用 key 索引表达式检索 map 时,如果检索的元素不存在会返回元素类型的零值,为了区分 map 中究竟是否存储了零值元素,可以使用ok-idiom语法检索 map:

m := make(map[string]int)
m["foo"] = 0
v1, exist := m["foo"]
fmt.Println(v1, exist)
v2, exist := m["bar"]
fmt.Println(v2, exist)
// Output:
// 0 true
// 0 false

使用 make 函数创建 map 时可以指定 map 的容量capacity,但初始容量并不会限制 map 的大小,map 的容量会自动增长以适应存储在其中的元素的数量。注意,不能使用cap函数获取 map 的容量,无法编译通过。

前面总结了 map 类型的一些基础知识,平时在使用 map 最容易犯错误的地方就是忘记 map 的初始化,尤其是当把 map 作为一个 struct 结构体的字段时,在初始化结构体时一定不要忘记初始化 map 字段。

type job struct {
	jobFuncs map[string]func()
}

另外需要特别注意的是,go 内置的 map 类型不是并发安全的,在运行时并发读写 map 时会有检查,如果出现问题会直接 panic。例如下面的测试例子,重复执行多次,可能会出现panic: fatal error: concurrent map read and map write

func TestMapConcurrentIssue(t *testing.T) {
 m := map[string]int{}
 go func() {
  for {
   m["foo"] = 1
  }
 }()

 go func() {
  for {
   t.Log(m["foo"])
  }
 }()
 time.Sleep(2 * time.Second)
}

并发访问 map

因为 go 内置的 map 类型不是并发安全的,为了在并发执行的 goroutine 间从 map 中读取和写入元素到 map 中,需要借助同步机制,比较常见的一种方式是使用sync.RWMutex。具体的实现形式是单独使用sync.RWMutex锁,或者将 map 和sync.RWMutex封装到一个新的结构体中。这里先引用一下 Go 的官方博客里Go maps in action中的例子:

定义一个计数器变量counter,基于一个匿名结构体,匿名结构体中包含一个 map 和一个 sync.RWMutex。

ar 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()

更好的方式是封装成一个具名结构体:

type Counter struct {
 m     map[string]int
 mutex sync.RWMutex
}

func NewCounter() *Counter {
 return &Counter{
  m: map[string]int{},
 }
}

func (c *Counter) Incr(key string) {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 c.m[key]++
}

func (c *Counter) Get(key string) int {
 c.mutex.RLock()
 defer c.mutex.RUnlock()
 return c.m[key]
}

func (c *Counter) Reset(key string) {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 delete(c.m, key)
}

上面的代码通过读写锁保证了 map 的并发安全,但是如果并发读写很大的情况下,对锁的竞争将十分激烈,如果要优化,就要根据具体的业务情况,需要减小锁的粒度。例如在 Counter 这个例子中能否通过数据分片的方式,将一把读写锁换成多个读写锁,每个锁负责一个分片的数据,这样可以一定程度提高并发的吞吐量。例如封装一个 Counters 结构体,内部包含若干个 Counter 作为分区,对某个 key 执行 counter 相关的操作时,需要先根据该 key 计算出该 key 所在的分区 counter,然后再在分区上调用具体的操作,这就减少了锁的粒度。

sync.Map

sync.Map是 go 1.9 引入的一个并发安全的 map。sync.Map的文档中是这样介绍它的: sync.Map类似 go 内建 map 类型声明为map[interface{}]interface{},但它对多个 goroutine 的并发访问是安全的,无需额外的锁或协调。文档中指出大多数场景下应该使用 go 内建的 map 类型在并发场景下搭配锁或协调的同步机制。sync.Map对两个常见的用例进行优化: (1)当给定 key 的元素只写一次,但会被读取很多次,类似缓存中的只增长的场景;(2)当多个 goroutine 读、写和更新不相交的键值对时。在这两种情况下使用sync.Map对比使用map搭配sync.RWMutex,可以明显减少锁竞争。

可以看出sync.Map只有在上面两个特定的场景下才会被建议使用,而且具体是否使用最好还要根据具体的性能测试来决定。下面简单介绍一下sync.Map的使用。

sync.Map的提供的方法如下:

type Map struct {
 // Has unexported fields.
}

func (m *Map) Delete(key interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Range(f func(key, value interface{}) bool)
func (m *Map) Store(key, value interface{})
  • Store方法用来设置一个键值对或者更新一个键值对
  • Load方法用来读取一个 key 对应的值,返回值 ok 用来指明是 key-value 否存在
  • Delete方法用来删除 key-value
  • Range用来迭代sync.Map,如果 f 函数返回 false 时,将会停止迭代
func TestSyncMap(t *testing.T) {
 m := &sync.Map{}
 go func() {
  for {
   m.Store("foo", 1)
  }
 }()

 go func() {
  for {
   t.Log(m.Load("foo"))
  }
 }()
 time.Sleep(2 * time.Second)
}

func TestForRangeSyncMap(t *testing.T) {
 m := &sync.Map{}
 m.Store("a", 1)
 m.Store("b", 2)
 m.Range(func(k, v interface{}) bool {
  t.Log(k, v)
  return true
 })
}

参考

  • The Go Programming Language Specification - Map types
  • Go maps in action - The Go Blog
  • Go pkg document
最近发表
标签列表