在2万字图解map文章主要讲述了Go中map的是如何实现的。本文将从应用的角度来总结map使用过程中容易出现的问题,如何保证map并发读写,以及并发读写的优化。
map申明的语法格式为var m map[K]V,这样就得到一个map对象m。需要注意的是,不是任何类型都是可以作为key,通过2万字图解map文章,我们知道map是通过key来定位存储数据在哪个位置的。key必须是可以比较的类型,即可以通过==和!=进行运算。但value类型就没有限制了,可以是任何类型,甚至是nil. 在Go语言中,可以比较的类型有布尔型、整数、浮点数、复数、字符串、通道channel、指针和接口,元素可比较的struct和数组也是可以比较的。不可比较的类型有:slice、map和函数值。下面申明的3个map都是还未初始化的,也就是他们现在都是nil值,直接向nil的map中添加数据会引发panic, 对nil的map查询key、删除key、遍历不会出现panic.
func main(){
var m1 map[int]int
var m2 map[string]struct{}
var m3 map[interface{}]int
}
map的初始化有两种方式:一种是通过make,另一种是{}.如下代码所示,m和m2和m3都是通过make初始化的,m4是直接在类型后面跟上{}初始化的。通过make初始化map可以指定map的大小也可以不指定大小。下面的m2指定初始化存储2个元素的map.一般在实际使用中,建议指定大小,减少扩容带来的性能影响。
func main() {
var m = make(map[int]int)
m2 := make(map[int]int, 2)
var m3 map[int]int
m3 = make(map[int]int)
m4 := map[int]int{}
}
map通过m[K]=V形式赋值,下面的操作向m中添加了两个元素,第三次赋值其实是更新操作,对于map中已有的key向里面添加数据是更新value.
func main() {
m := map[int]int{}
m[1] = 1
m[2] = 2
m[1] = 3
}
map中查询元素调用有两种形式,一种是只有一个返回值,另一种是有两个返回值,第一个返回值表示key对应的value,第二个返回值表示key是否在map中存在。当key不在map中时,返回的value不是nil而是对应value类型的零值,例如下面的m,取它的key为1的数据时,得到的v值为0,是int型的零值,所以不好区分返回的0是真正存储的1对应的0呢还是1不存在返回的零值。所以就有第二种返回两个参数的调用,第二返回值是一个bool类型,如果返回true表示key在map中,否则key不在map. 注意不能直接对map的value取地址,例如直接操作 &m[1]是不被允许的,编译器直接报错。因为map分配value空间可能因扩容失效,所以设计成不能取地址操作。
func main() {
m := map[int]int{}
v := m[1]
v2, ok := m[2]
_ = v
_ = v2
_ = ok
}
map通过for range进行遍历,具体的遍历形式也有两种,一种是同时遍历key和value,另一种是只遍历key.通过for range得到的key和value值是map中key和value的副本,修改下面k和v的值,并影响m.
func main() {
m := map[int]int{1: 1, 2: 2, 3: 3}
// 遍历所有的key-value值
for k, v := range m {
fmt.Println(k, v)
}
// 只遍历key值
for k := range m {
fmt.Println(k)
}
}
在单个goroutine中即没有并发冲突下,对map进行遍历的过程中进行删除操作是安全的,不会报错误。边遍历边删除操作,删除的key不会被遍历出来。例如下面的例子中,在遍历的过程中删除key,执行完遍历之后,m中的所有数据也删除完了,没有问题。
// 边遍历边删除是可以的,没有问题
func main() {
m := map[int]int{1: 1, 2: 2, 3: 3}
for k := range m {
delete(m, k)
}
}
下面对map边遍历边添加也是可以的,不过添加的4和5可能不被打印出来,也可能被打印出来,多次运行该程序,也可能输出的结果不同。具体原因,可以看2万字图解map文章,理解map的迭代位置是随机之后很容易理解这里。
func main() {
m := map[int]int{1: 1, 2: 2, 3: 3}
// 对m边遍历遍添加也是可以的,不过添加的4和5会被输出出来,也可能不会输出出来
// 多次运行输出的结果可能不同,因为map的输出顺序是不固定的,添加的元素可能被
// 访问到也可能不被访问到
for k, v := range m {
fmt.Println(k, v)
if k == 1 {
m[4] = 4
}
if k == 2 {
m[5] = 5
}
}
}
删除map中的某个key直接使用delete函数。Go中没有提供一次性删除key中所有数据的方法,可以遍历map,对每个key执行delete操作。不过可以将map指向一个新的map或者置为nil,达到间接删除的目的,之前map中的旧数据会被GC回收掉。
func main() {
m := map[int]int{1: 1, 2: 2, 3: 3}
// 通过遍历删除map中所有的key
for k := range m {
delete(m, k)
}
//将m置nil,之前的数据会被GC回收
m = nil
//将m指向新的地址,之前的数据会被GC回收
m = make(map[int]int)
}
对map进行delete操作,底层将key对应的tophash设置为了一个emptyOne标记,对于非直接引用对象,底层内存不会真正释放,可能会导致内存一直增长造成OOM.下面结合一个例子,说明这个现象。
package main
import (
"fmt"
"runtime"
)
const (
count = 10000
)
var m map[int]int
func main() {
fmt.Println("向map添加数据前:")
printMemStats()
addMap()
runtime.GC()
fmt.Printf("向map添加%d个数据后:\n", count)
printMemStats()
deleteMap()
runtime.GC()
fmt.Printf("删除map中%d个数据后:\n", count)
printMemStats()
m = nil
runtime.GC()
fmt.Println("将map设置为nil后:")
printMemStats()
}
func addMap() {
if m == nil {
m = make(map[int]int, count)
}
for i := 0; i < count; i++ {
m[i] = i
}
}
func deleteMap() {
for i := 0; i < count; i++ {
delete(m, i)
}
}
func printMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %vKB, TotalAlloc = %vKB, Sys = %vKB, NumGC = %v\n\n", m.Alloc/1024, m.TotalAlloc/1024, m.Sys/1024, m.NumGC)
}
程序输出结果为

这里对上面输出结果的字段含义做一个说明,Alloc是已分配对象的字节数,TotalAlloc是分配的字节数累积之和,对象释放的时候这个值并不会减少,Sys是从操作系统获得的内存总数,NumGC是垃圾回收的次数。
map对象不是线程(goroutine)安全的,并发读写的时候在运行时会有检查,如果出现并发读写的问题就会产生panic. 下面的程序同时开启了2个goroutine对m进行操作,一个读一个写,执行下面的程序会报fatal error: concurrent map read and map write
package main
import "sync"
func main() {
var m = make(map[int]int, 2)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for {
m[1] = 1
}
}()
go func() {
defer wg.Done()
for {
_ = m[2]
}
}()
wg.Wait()
}
如何避免map并发读写出现的panic问题呢?通常的做法是对map进行加锁,可以使用读写锁,来提供程序性能。具体来说将map和锁封装到一个结构体里面,对外提供查询、删除、添加和修改map的接口,内部实现加锁处理。可以看下面这个实例。
package main
import (
"sync"
"time"
)
type rwMap struct {
sync.RWMutex
m map[int]int
}
func NewrwMap() *rwMap {
return &rwMap{
m: make(map[int]int),
}
}
func (m *rwMap) Set(k, v int) {
m.Lock()
defer m.Unlock()
m.m[k] = v
}
func (m *rwMap) Get(k int) (int, bool) {
m.RLock()
defer m.RUnlock()
v, ok := m.m[k]
return v, ok
}
func (m *rwMap) Delete(k int) {
m.Lock()
defer m.Unlock()
delete(m.m, k)
}
func (m *rwMap) Len() int {
m.RLock()
defer m.RUnlock()
return len(m.m)
}
func main() {
m := NewrwMap()
go func() {
for {
m.Set(1, 1)
}
}()
go func() {
for {
_, _ = m.Get(2)
}
}()
time.Sleep(time.Second * 5)
}
上面的例子已说明可以使用读写锁来提供线程(goroutine)安全的map,但是在大量并发读写的时候,锁的竞争非常激烈。锁是性能下降的一个重要原因之一,所以能不使用锁就不要使用。对于map来说,我们可以通过减少锁的粒度和锁的持有时间来降低锁带来的性能影响。分片map就是将map的一把大锁分成几把小锁,每个锁控制一个分片,每个分片是一个map,实际上就是一个map数组。下面是一个分片map的简单实例,具体更详细的学习可以研究github上orcaman/concurrent-map实现。
package main
import (
"sync"
)
var shardCount = 32
type ConcurrentMap []*concurrentMapShared
type concurrentMapShared struct {
sync.RWMutex
items map[string]interface{}
}
func New() ConcurrentMap {
m := make(ConcurrentMap, shardCount)
for i := 0; i < shardCount; i++ {
m[i] = &concurrentMapShared{items: make(map[string]interface{})}
}
return m
}
func (m ConcurrentMap) GetShard(key string) *concurrentMapShared {
return m[uint(fnv32(key))%uint(shardCount)]
}
func fnv32(key string) uint32 {
hash := uint32(2166136261)
const prime32 = uint32(16777619)
keyLength := len(key)
for i := 0; i < keyLength; i++ {
hash *= prime32
hash ^= uint32(key[i])
}
return hash
}
func (m ConcurrentMap) Set(key string, value interface{}) {
shard := m.GetShard(key)
shard.Lock()
shard.items[key] = value
shard.Unlock()
}
func (m ConcurrentMap) Get(key string) (interface{}, bool) {
shard := m.GetShard(key)
shard.RLock()
value, ok := shard.items[key]
shard.RUnlock()
return value, ok
}
上面是分片的map的一个简单实现,下面将其与非分片map进行一个对比性能验证,让我们对分片map的性能提升有一个感性认识。注意的是下面只是一个简单验证,得到的性能提升效果并不是非常准确的,在实际的项目中使用的时候要专门的性能验证。
package main
import (
"strconv"
"sync"
"testing"
)
// 验证分片map的性能
func BenchmarkConcurrentMap(b *testing.B) {
var cm = New()
for i := 0; i < b.N; i++ {
cm.Set(strconv.Itoa(i%100), i)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
go func(i int) {
cm.Get(strconv.Itoa(i % 100))
}(i)
go func(i int) {
cm.Set(strconv.Itoa((i+1)%100), i)
}(i)
}
}
// 验证非分片map的性能
func BenchmarkMap(b *testing.B) {
var m = RWMap{m: make(map[int]int)}
for i := 0; i < b.N; i++ {
m.Set(i%100, i)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
go func(i int) {
m.Get(i % 100)
}(i)
go func(i int) {
m.Set((i+1)%100, i)
}(i)
}
}
type RWMap struct {
sync.RWMutex
m map[int]int
}
func (m *RWMap) Get(k int) (int, bool) {
m.RLock()
v, ok := m.m[k]
m.RUnlock()
return v, ok
}
func (m *RWMap) Set(k, v int) {
m.Lock()
m.m[k] = v
m.Unlock()
}
得到性能验证结果如下,可以看到分片map相比非分片map性能提升近1倍。


本文从应用层面总结了map的基本操作以及使用不当可能引发的问题,概括起来有以下几点
Golang map 如何进行删除操作[1]map delete存在的问题[2]
[1]
Golang map 如何进行删除操作: https://zhuanlan.zhihu.com/p/30662267
[2]
map delete存在的问题: https://github.com/golang/go/issues/20135