余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

Go中使用Map闭坑指南和源码分析

xiyangw 2023-09-16 15:06 13 浏览 0 评论

1.map的基本使用

 map[K]V

注意点:

  1. key类型必须是可比较的(comparable),也就是可以使用 ==和 !=操作符进行比较
  2. 可比较的类型有 bool,整数,浮点数,复数,字符串,指针,channel,接口,包括可比较元素的struct和数组 都是可比较的
  3. slice,map,函数值都是不可比较的
  4. 如果使用struct做为key时,需要注意,如果struct中的某个值修改了,在map中是无法获取到的,如下代码:
type mapKey struct {
	key int
}

func TestPool(t *testing.T) {
	var m = make(map[mapKey]string)
	var key = mapKey{10}
	m[key] = "hello"
	t.Log(m[key])
	key.key = 100 //重新给key赋值
	t.Log(m[key]) //这里无法获取到相关值
}


2.map的返回值

 v,ok :=map[key]

说明:

  1. map[key]函数返回结果可以是一个值,也可以两个值
  2. 区分真正的零值和key不存在这两种情况,需要根据第二个返回值来区分,如果第二值返回true,说明值存在,为false,则说明key不存在
  3. map是无序的,如里要对map排序,可以根据 map里的key先进行排序,再根据key,输出map的值,如下排序
func TestMapSort(t *testing.T) {
	var m = make(map[int]int)
	var s []int
	for i := 0; i < 10; i++ {
		m[i] = i
		s = append(s, i)
	}
	sort.Ints(s)
	t.Log(s)
	for i := range s {
		t.Logf("val:%d", m[i])
	}
}

3.使用map的常见错误

3.1 未初始化

  1. 使用map必须使用make初始化,否则赋值时报panic
  2. 从一个nil的map对象中获取数据时,不会panic,会得到零值

3.2 并发读写

go内建的map对象不是线程安全的,有并发问题,直接panic,如下代码,会报fatal error: concurrent map read and map write

 func TestGo(t *testing.T) {
     var m = make(map[int]int, 10)
     go func() {
         for {
             m[1] = 1
         }
     }()
     go func() {
         for {
             _ = m[2]
         }
     }()
     select {}
 }

4. 实现线程安全的map

自己实现的线程安全的map

 type RWMap struct {
     mu sync.RWMutex
     m  map[int]int
 }
 
 func NewRWMap(n int) *RWMap {
     return &RWMap{
         m: make(map[int]int, n),
     }
 }
 func (r *RWMap) Get(k int) (int, bool) {
     r.mu.RLock()
     defer r.mu.RUnlock()
     v, ok := r.m[k]
     return v, ok
 }
 func (r *RWMap) Set(k, v int) {
     r.mu.Lock()
     defer r.mu.Unlock()
     r.m[k] = v
 }
 func (r *RWMap) Delete(k int) {
     r.mu.Lock()
     defer r.mu.Unlock()
     delete(r.m, k)
 }
 func (r *RWMap) Len() int {
     r.mu.RLock()
     defer r.mu.RUnlock()
     return len(r.m)
 }
 func (r *RWMap) Each(f func(k, v int) bool) {
     r.mu.RLock()
     defer r.mu.RUnlock()
     for k, v := range r.m {
         f(k, v)
     }
 }
 //测试
 func TestSafeMap(t *testing.T) {
     m := NewRWMap(10)
     go func() {
         for {
             m.Set(1, 100)
             time.Sleep(time.Second)
         }
     }()
     go func() {
         for {
             v, _ := m.Get(1)
             fmt.Println(v)
             time.Sleep(time.Second)
         }
 
     }()
     select {}
 }

5.更高效的并发map: 分片加锁

原则: 尽量减少锁的粒度和锁的持有时间

常用的分片加锁map 第三方库 orcaman/concurrent-map

 //使用方法
 import (
     "github.com/orcaman/concurrent-map/v2"
 )
 //创建一个map
     m := cmap.New[string]()
 
     // 设置值
     m.Set("foo", "bar")
 
     // 获取值
     bar, ok := m.Get("foo")
 
     // 删除一个值
     m.Remove("foo")

6.Go内置的线程安全map

6.1 常用方法

  • Store(key, value any)
  • Load(key any)
  • Delete( key any)
  • LoadOrStore(key, value any)
  • LoadAndDelete(key any)
  • Range(f func(key, value any) bool)

6.2 源码分析

 type Map struct {
     mu Mutex
 //基本上你可以把它看成一个安全的只读的map
     //它包含的元素其实也是通过原子操作更新的,但是已删除的entry就需要加锁操作了
     read atomic.Value // readOnly
 //包含需要加锁才能访问的元素
 // 包括所有在read字段中但未被expunged(删除)的元素以及新加的元素
 
     dirty map[any]*entry
 
 //记录从read中读取miss的次数,一旦miss数和dirty长度一样了,就会把dirty提升为read
     misses int
 }
 type readOnly struct {
     m       map[any]*entry
     amended bool // 当dirty中包含read没有的数据时为true,比如新增一条数据.
 }
 //代表一个值
 type entry struct {
     
     p unsafe.Pointer // *interface{}
 }

说明:

  1. 为了减少锁对性能的影响,这里采用的空间换时间,通过冗余的两个数据结构 (只读的read 字段,可写的dirty),对只读字段操作不用加锁
  1. 优先从read字段读取,更新,删除,对read操作不用加锁
  2. 动态调整,如里miss次数多了之后,将dirty数据提升为read,避免总是对dirty加锁
  3. double-checking操作,加锁之后还要再检查read字段,只有真的不存在后才操作dirty字段
  4. 延迟删除,删除一个键时,只是打标记,只有把dirty提升到read字段的时候,才会清理删除的数据

Store方法

 func (m *Map) Store(key, value any) {
     read, _ := m.read.Load().(readOnly)
     //如果read中有值,说明是更新,直接通过cas操作即可
     if e, ok := read.m[key]; ok && e.tryStore(&value) {
         return
     }
 
     m.mu.Lock()
     //如里read中不存在或cas时失败
     read, _ = m.read.Load().(readOnly)
     if e, ok := read.m[key]; ok { //双检查,看read是否已经存在
         if e.unexpungeLocked() {
     //此项目先前已经被删除了,通过将它的值设置为nil,标记为unexpunged
             m.dirty[key] = e
         }
         e.storeLocked(&value)
     } else if e, ok := m.dirty[key]; ok { // 如果dirty中有此项 直接更新
         e.storeLocked(&value)
     } else {
         //到这里说明是一个新key
         if !read.amended { //如里dirty为nil
         //需要创建dirty对象,并且标记read的amended为true,
         // 说明有元素它不包含而dirty包含
             m.dirtyLocked()
             m.read.Store(readOnly{m: read.m, amended: true})
         }
         m.dirty[key] = newEntry(value) //将新值增加到dirty对象中
     }
     m.mu.Unlock()
 }
 func (m *Map) dirtyLocked() {
     if m.dirty != nil { //如果dirty字段已经存在,不需要创建了
         return
     }
 
     read, _ := m.read.Load().(readOnly) // 获取read字段
     m.dirty = make(map[any]*entry, len(read.m))
     for k, e := range read.m { // 遍历read字段
         if !e.tryExpungeLocked() { // 把非punged的键值对复制到dirty中
             m.dirty[k] = e
         }
     }
 }

Load 方法

 func (m *Map) Load(key any) (value any, ok bool) {
     read, _ := m.read.Load().(readOnly) //首先从read处理
     e, ok := read.m[key]
     if !ok && read.amended { //如果不存在并且dirty不为nil(有新的元素)
         m.mu.Lock()
 // 双检查,看看read中现在是否存在此key
         read, _ = m.read.Load().(readOnly)
         e, ok = read.m[key]
         if !ok && read.amended { //依然不存在,并且dirty不为nil
             e, ok = m.dirty[key] // 从dirty中读取
             // 不管dirty中存不存在,miss数都加1
             m.missLocked()
         }
         m.mu.Unlock()
     }
     if !ok {
         return nil, false
     }
     return e.load() //返回读取的对象,e既可能是从read中获得的,也可能是从dirty中获得的
 }
 func (m *Map) missLocked() {
     m.misses++ // misses计数加一
     if m.misses < len(m.dirty) { // 如果没达到阈值(dirty字段的长度),返回
         return
     }
     m.read.Store(readOnly{m: m.dirty}) //把dirty字段的内存提升为read字段
     m.dirty = nil // 清空dirty
     m.misses = 0 // misses数重置为0
 }

Delete方法

 func (m *Map) Delete(key any) {
     m.LoadAndDelete(key)
 }
 func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
     read, _ := m.read.Load().(readOnly)
     e, ok := read.m[key]
     if !ok && read.amended {
         m.mu.Lock()
         //双检查
         read, _ = m.read.Load().(readOnly)
         e, ok = read.m[key]
         if !ok && read.amended {
             e, ok = m.dirty[key]
             delete(m.dirty, key)
         // miss数加1
             m.missLocked()
         }
         m.mu.Unlock()
     }
     if ok {
         return e.delete()
     }
     return nil, false
 }



相关推荐

数控系统常见术语详解,机加工人士必备资料
数控系统常见术语详解,机加工人士必备资料

增量编码器(Incrementpulsecoder)回转式位置测量元件,装于电动机轴或滚珠丝杠上,回转时发出等间隔脉冲表示位移量。由于没有记忆元件,故不能准...

2023-09-24 17:42 xiyangw

功、功率、扭矩的关系

功=功率×时间work=power×timeW=P×T功=力×距离work=force×lengthW=F×LP×T=F×LP=F×L/T=F×V(velocity)具体到电机输出轴上,圆...

Wi-Fi协议(802.11 )常见专业术语汇总
Wi-Fi协议(802.11 )常见专业术语汇总

Wi-Fi协议(802.11)常见专业术语汇总AP(Accesspoint的简称,即访问点,接入点):是一个无线网络中的特殊节点,通过这个节点,无线网络中的...

2023-09-24 17:41 xiyangw

不需要策略模式也能避免满屏if/else
不需要策略模式也能避免满屏if/else

满屏if/elsejava复制代码publicstaticvoidmain(String[]args){inta=1;if...

2023-09-24 17:41 xiyangw

喜极而泣,我终于干掉了该死的 if-else
喜极而泣,我终于干掉了该死的 if-else

推荐阅读:面试淘宝被Tomcat面到“自闭”,学习这份文档之后“吊打”面试官刷完spring+redis+负载均衡+netty+kafka面试题,再去面试BAT...

2023-09-24 17:40 xiyangw

Python中使用三元运算符简化if-else语句
Python中使用三元运算符简化if-else语句

Python是一种极简主义的编程语言,相比其他编程语言,在多个地方简化了代码的写法,可以让我们用更少的时间更简洁地完成工作。以赋值运算符为例:a=a+b简化...

2023-09-24 17:40 xiyangw

雅思课堂 | 雅思口语写作句型第二讲
雅思课堂 | 雅思口语写作句型第二讲

纯干货,无废话用最少的时间学最制胜的内容!泡图书馆泡不过学霸?碎片时间也能弯道超车!向着雅思8分行动起来吧!雅思口语写作句型1.Ipreferseeing...

2023-09-24 17:39 xiyangw

设计模式(三)——简单的状态模式代替if-else
设计模式(三)——简单的状态模式代替if-else

博主将会针对Java面试题写一组文章,包括J2ee,SQL,主流Web框架,中间件等面试过程中面试官经常问的问题,欢迎大家关注。一起学习,一起成长。前言大多数开...

2023-09-24 17:38 xiyangw

如何优化代码中大量的if/else,switch/case?

前言随着项目的迭代,代码中存在的分支判断可能会越来越多,当里面涉及到的逻辑比较复杂或者分支数量实在是多的难以维护的时候,我们就要考虑下,有办法能让这些代码变得更优雅吗?正文使用枚举这里我们简单的定义一...

优秀程序员早就学会用“状态模式”代替if-else了
优秀程序员早就学会用“状态模式”代替if-else了

2020年已经进入倒计时了,大家立好的flag完成了吗?2020实“鼠”不易,希望2021可以“牛”转乾坤。简介状态模式是行为型设计模式的一种。其设计理念是当对...

2023-09-24 17:37 xiyangw

用Select Case语句对执行多条件进行控制
用Select Case语句对执行多条件进行控制

今日的内容是"VBA之EXCEL应用"的第六章"条件判断语句(If...Then...Else)在VBA中的利用"。这讲是第三节...

2023-09-24 17:37 xiyangw

c#入门教程(四)条件判断if else

条件判断,是编程里常用的判断语句,比如某个代码如果满足条件就执行a代码块否则就执行b代码块。案例1:inti=2*5;if(a>0){执行a代码块}elseif(a<0){执行b代码块...

每日学编程之JAVA(十一)—条件语句(if……else)

一个if语句包含一个布尔表达式和一条或多条语句。如果布尔表达式的值为true,则执行if语句中的代码块,否则执行if语句块后面的代码。if语句后面可以跟else语句,当if语句...

不需要策略模式也能避免满屏if/else

除了使用策略模式以外,还可以使用其他设计模式来避免满屏if/else的问题。以下是一些可能的解决方案:工厂模式:将if/else语句移到工厂类中,由工厂类负责创建对象。这样可以将if/else语句从客...

围绕ifelse与业务逻辑的那些梗
围绕ifelse与业务逻辑的那些梗

ifelse很重要,几乎是程序员编程核心,业务逻辑与规则也通过ifelse体现出来,语句简单但是背后文章很大,先看几则幽默图:1.也许默认使用returnf...

2023-09-24 17:36 xiyangw

取消回复欢迎 发表评论: