网站首页 > 技术文章 正文
哈希表(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 的类型就不能是func、map和slice。 如果 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
猜你喜欢
- 2024-10-01 朴实无华,Python 100 例,总有一例你喜欢
- 2024-10-01 零基础快速入门Python难点函数详解附案例,30分钟即可学会掌握
- 2024-10-01 Python的一些日常高频写法(python小技巧)
- 2024-10-01 容易忽略的数据类型:集合(set)(set集合的作用)
- 2024-10-01 Python 工匠:做一个精通规则的玩家
- 2024-10-01 python内置函数(查看变量的python内置函数)
- 2024-10-01 写了三年代码,还是不懂 Python 世界的规则
- 2024-10-01 RealPython 基础教程:Python 字典用法详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)