• GoFrame的gmap相比Go原生的map,天然支持排序和有序遍历!?


    大家好,我是阳哥。内容比较硬核,建议先收藏再观看。

    我也在B站发布了这期内容的视频版,视频相比文章看起来确实更通俗易懂。
    如果你是初学者建议先看视频:欢迎大家点击这个链接观看
    觉得不错,欢迎关注、三连一波。谢谢!

    如果你有经验,想节省时间,请直接阅读文章:

    前言

    有好多初学GO和GoFrame的小伙伴搞不清楚map怎么用。

    不少刚入门的小伙伴都被Go语言中map的无序性“坑过”,尤其是PHP转Go的小伙伴,毕竟用惯了PHP的数组。

    这篇文章就是给初学的小伙伴们答疑解惑的,会为大家介绍:

    为什么Go语言中的map是无序的,如何自定义实现map的排序?

    (Ps:这部分不作为这篇文章的重点,感兴趣的小伙伴可以看我之前整理的这篇文章:# Go容易搞错的知识点汇总:Go map如何实现排序 部分

    GoFrame的gmap相比于Go原生的map有什么优势?为什么天然支持排序和有序遍历!?

    先说结论

    GoFrame提供的gmap字典类型,包含多个数据结构的map容器:HashMapTreeMapListMap。其中TreeMap支持排序,TreeMapListMap支持有序遍历。

    使用技巧

    我们在使用GoFrame的gmap时,要结合自己的场景使用合适的map容器:

    1. 当我们对返回顺序有要求时不能使用HashMap,因为HashMap返回的是无序列表;
    2. 当需要按输入顺序返回结果时使用ListMap
    3. 当需要让返回结果按照自然升序排列时使用TreeMap

    注意:gmap的实例化默认是HashMap类型:hashMap := gmap.New(true)

    一图胜千言

    GoFrame gmap 基本介绍:

    支持并发安全开关选项的map容器,最常用的数据结构。

    该模块包含多个数据结构的map容器:HashMapTreeMapListMap

    在这里插入图片描述

    实例化示例:

       hashMap := gmap.New(true)
       listMap := gmap.NewListMap(true)
       treeMap := gmap.NewTreeMap(gutil.ComparatorInt, true)
    
    • 1
    • 2
    • 3

    实践得真知

    package main
    
    import (
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
       "github.com/gogf/gf/v2/frame/g"
       "github.com/gogf/gf/v2/util/gutil"
    )
    
    func main() {
       array := g.Slice{1, 5, 2, 3, 4, 6, 8, 7, 9}
       hashMap := gmap.New(true)
       listMap := gmap.NewListMap(true)
       treeMap := gmap.NewTreeMap(gutil.ComparatorInt, true)
       for _, v := range array {
          hashMap.Set(v, v)
       }
       for _, v := range array {
          listMap.Set(v, v)
       }
       for _, v := range array {
          treeMap.Set(v, v)
       }
    
       fmt.Println("HashMap   Keys:", hashMap.Keys())   //HashMap   Keys: [7 9 1 5 2 4 6 3 8]
       fmt.Println("HashMap Values:", hashMap.Values()) //HashMap Values: [6 7 9 1 5 2 4 3 8]
       //从打印结果可知hashmap的键列表和值列表返回值的顺序没有规律,随机返回
       fmt.Println("ListMap   Keys:", listMap.Keys())   //ListMap   Keys: [1 5 2 3 4 6 8 7 9]
       fmt.Println("ListMap Values:", listMap.Values()) //ListMap Values: [1 5 2 3 4 6 8 7 9]
       //listmap键列表和值列表有序返回,且顺序和写入顺序一致
       fmt.Println("TreeMap   Keys:", treeMap.Keys())   //TreeMap   Keys: [1 2 3 4 5 6 7 8 9]
       fmt.Println("TreeMap Values:", treeMap.Values()) //TreeMap Values: [1 2 3 4 5 6 7 8 9]
       //treemap键列表和值列表也有序返回,但是不和写入顺序一致,按自然数升序返回
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    打印结果

    通过打印结果我们可以发现:

    1. hashmap的键列表和值列表返回值的顺序没有规律,随机返回

    2. listmap键列表和值列表有序返回,且顺序和写入顺序一致

    3. treemap键列表和值列表也有序返回,但是不和写入顺序一致,按自然数升序返回

    这也佐证了我开篇提到的使用技巧。

    在这里插入图片描述

    为了让大家更好的理解gmap,下面介绍一下gmap的基础使用和一些进阶技巧。

    基础概念

    GoFrame框架(下文简称gf)提供的数据类型,比如:字典gmap、数组garray、集合gset、队列gqueue、树形结构gtree、链表glist都是支持设置并发安全开关的。

    支持设置并发安全开关这也是gf提供的常用数据类型和原生数据类型重要的区别之一。

    对比sync.Map

    Go语言提供的原生map不是并发安全的map类型

    Go语言从1.9版本开始引入了并发安全的sync.Map,但gmap比较于标准库的sync.Map性能更加优异,并且功能更加丰富。

    goos: linux
    goarch: amd64
    Benchmark_GMapSet-4                     10000000               209 ns/op              15 B/op          0 allocs/op
    Benchmark_SyncMapSet-4                   3000000               451 ns/op              67 B/op          3 allocs/op
    Benchmark_GMapGet-4                     30000000              66.4 ns/op               0 B/op          0 allocs/op
    Benchmark_SyncMapGet-4                  30000000              36.0 ns/op               0 B/op          0 allocs/op
    Benchmark_GMapRemove-4                  10000000               207 ns/op               0 B/op          0 allocs/op
    Benchmark_SyncMapRmove-4                30000000              42.4 ns/op               0 B/op          0 allocs/op
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对性能测试感兴趣的小伙伴可以详细看下官方文档的介绍,不作为这篇文章的重点。

    基础使用

    1. gmap.New(true) 在初始化的时候开启并发安全开关
    2. 通过 Set() 方法赋值,通过 Sets() 方法批量赋值
    3. 通过 Size() 方法获取map大小
    4. 通过 Get() 根据key获取value值

    更多操作大家可以直接查看下方的代码示例,也欢迎大家动手复刻

    为了方便大家更好的查看效果,在下方代码段中标明了打印结果:

    package main
    
    import (
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
    )
    
    func main() {
       m := gmap.New(true)
       // 设置键值对
       for i := 0; i < 10; i++ {
          m.Set(i, i)
       }
       fmt.Println("查询map大小:", m.Size())
    
       //批量设置键值对
       m.Sets(map[interface{}]interface{}{
          10: 10,
          11: 11,
       })
    
       // 目前map的值
       fmt.Println("目前map的值:", m)
    
       fmt.Println("查询是否存在键值对:", m.Contains(1))
    
       fmt.Println("根据key获得value:", m.Get(1))
    
       fmt.Println("删除数据", m.Remove(1))
    
       //删除多组数据
       fmt.Println("删除前的map大小:", m.Size())
       m.Removes([]interface{}{2, 3})
       fmt.Println("删除后的map大小:", m.Size())
    
       //当前键名列表
       fmt.Println("键名列表:", m.Keys())   //我们发现是无序列表
       fmt.Println("键值列表:", m.Values()) //我们发现也是无序列表
    
       //查询键名,当键值不存在时写入默认值
       fmt.Println(m.GetOrSet(20, 20))   //返回值是20
       fmt.Println(m.GetOrSet(20, "二十")) //返回值仍然是20,因为key对应的值存在
       m.Remove(20)
       fmt.Println(m.GetOrSet(20, "二十")) //返回值是二十,因为key对应的值不存在
    
       // 遍历map
       m.Iterator(func(k interface{}, v interface{}) bool {
          fmt.Printf("%v:%v \n", k, v)
          return true
       })
    
       //自定义写锁操作
       m.LockFunc(func(m map[interface{}]interface{}) {
          m[88] = 88
       })
    
       // 自定义读锁操作
       m.RLockFunc(func(m map[interface{}]interface{}) {
          fmt.Println("m[88]:", m[88])
       })
    
       // 清空map
       m.Clear()
    
       //判断map是否为空
       fmt.Println("m.IsEmpty():", m.IsEmpty())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    运行结果

    在这里插入图片描述

    上面介绍的基础使用比较简单,下面介绍进阶使用。

    合并 merge

    注意:Merge()的参数需要是map的引用类型,也就是参数需要传map的取址符。

    package main
    
    import (
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
    )
    
    func main() {
       var m1, m2 gmap.Map
       m1.Set("k1", "v1")
       m2.Set("k2", "v2")
       m1.Merge(&m2)
       fmt.Println("m1.Map()", m1.Map()) //m1.Map() map[k1:v1 k2:v2]
       fmt.Println("m2.Map()", m2.Map()) //m2.Map() map[k2:v2]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    打印结果

    在这里插入图片描述

    序列化

    正如之前的文章 GoFrame glist 基础使用和自定义遍历 介绍的,gf框架提供的数据类型不仅支持设置并发安全开关,也都支持序列化和反序列化。

    json序列化和反序列化:序列化就是转成json格式,反序列化就是json转成其他格式类型(比如:map、数组、对象等)

    package main
    
    import (
       "encoding/json"
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
    )
    
    func main() {
       // 序列化
       //var m gmap.Map
       m := gmap.New() //必须实例化 只是像上面声明但是不进行实例化,是无法序列化成功的
       m.Sets(map[interface{}]interface{}{
          "name": "王中阳",
          "age":  28,
       })
       res, _ := json.Marshal(m)
       fmt.Println("序列化结果:", string(res)) //打印结果:{"age":28,"name":"王中阳"}
    
       // 反序列化
       m2 := gmap.New()
       s := []byte(`{"age":28,"name":"王中阳"}`)
       _ = json.Unmarshal(s, &m2)
       fmt.Println("反序列化结果:", m2.Map()) //反序列化结果: map[age:28 name:王中阳]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    打印结果

    在这里插入图片描述

    踩坑

    正如上面代码段中注释掉的://var m gmap.Map

    在进行序列化操作时,必须实例化map

    m := gmap.New() 
    
    • 1

    在这里插入图片描述

    只是声明map而不进行实例化,是无法序列化成功的

    var m gmap.Map
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xLCfmugd-1667438337205)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe935778374e4169a02c3c881ac17a9f~tplv-k3u1fbpfcp-watermark.image?)]

    另外一个需要注意的知识点就是过滤空值了:

    过滤空值

    package main
    
    import (
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
    )
    
    func main() {
       //首先明确:空值和nil是不一样的,nil是未定义;而空值包括空字符串,false、0等
       m1 := gmap.NewFrom(map[interface{}]interface{}{
          "k1": "",
          "k2": nil,
          "k3": 0,
          "k4": false,
          "k5": 1,
       })
    
       m2 := gmap.NewFrom(map[interface{}]interface{}{
          "k1": "",
          "k2": nil,
          "k3": 0,
          "k4": false,
          "k5": 1,
       })
    
       m1.FilterEmpty()
       m2.FilterNil()
    
       fmt.Println("m1.FilterEmpty():", m1) //预测结果: k5:1
       fmt.Println("m2.FilterNil():", m2)   //预测结果:除了k2,其他都返回
    
       // 打印结果和预期的一致:
       //m1.FilterEmpty(): {"k5":1}
       //m2.FilterNil(): {"k1":"","k3":0,"k4":false,"k5":1}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    打印结果

    在这里插入图片描述

    还有一个非常好用的特性,键值对反转:

    键值对反转 Flip

    package main
    
    import (
       "github.com/gogf/gf/v2/container/gmap"
       "github.com/gogf/gf/v2/frame/g"
    )
    
    func main() {
       // 键值对反转flip
       var m gmap.Map
       m.Sets(map[interface{}]interface{}{
          "k1": "v1",
          "k2": "v2",
       })
       fmt.Println("反转前:", m.Map())
       m.Flip()
       fmt.Println("反转后:", m.Map())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    打印结果

    在这里插入图片描述

    出栈(随机出栈)

    这个出栈的知识点和我开篇的使用技巧呼应上了:

    package main
    
    import (
       "fmt"
       "github.com/gogf/gf/v2/container/gmap"
    )
    
    func main() {
       //pop pops map出栈(弹栈)
       var m gmap.Map
       m.Sets(map[interface{}]interface{}{
          1: 1,
          2: 2,
          3: 3,
          4: 4,
          5: 5,
       })
    
       fmt.Println("m.Pop()之前:", m.Map())
       key, value := m.Pop()
       fmt.Println("key:", key)
       fmt.Println("value:", value)
       fmt.Println("m.Pop()之后:", m.Map()) //多次测试后发现是随机出栈,不能理所当然的认为按顺序出栈
    
       res := m.Pops(2) //参数是出栈个数
       fmt.Println("res:", res)
       fmt.Println("m.Pops之后:", m.Map()) //多次测试之后发现也是随机出栈
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行结果

    在这里插入图片描述

    踩坑

    注意:多次测试后发现是随机出栈,不能理所当然的认为按顺序出栈。

    我们深入思考一下原因:其实很简单,因为gmap的底层实现是hashmap,本身就是无序的,当然不可能按顺序出栈了。

    总结

    好了,我们再来回顾一下这篇文章的重点:

    1. 我们在使用GoFrame的gmap时,要结合自己的场景使用合适的map容器:
      1. 当我们对返回顺序有要求时不能使用HashMap,因为HashMap返回的是无序列表;
      2. 当需要按输入顺序返回结果时使用ListMap
      3. 当需要让返回结果按照自然升序排列时使用TreeMap
      4. gmap的实例化默认是HashMap类型:hashMap := gmap.New(true)
    2. gmap的基础使用和进阶使用技巧:反转map、序列化、合并map、出栈等。
    3. gf框架提供的数据结构,比如:字典gmap、数组garray、集合gset、队列gqueue、树形结构gtree、链表glist 都是支持设置并发安全开关的;而且都支持序列化和反序列化,实现了标准库json数据格式的序列化/反序列化接口。

    一起学习

  • 相关阅读:
    为什么说想到Python中的装饰器是天才
    3、Sentinel 动态限流规则
    iText7画发票PDF——小tips
    基于YOLOv8的目标跟踪——汽车跟踪和计数
    【Java八股文总结】之JVM
    产品推荐 | 基于Anlogic系列EG4S20 FPGA开发板
    使用Vue3+ElementPlus+高德地图实现在浏览器中搜索地点并被标记在地图中
    相似性搜索:第 3 部分--混合倒排文件索引和产品量化
    单线程的 Redis 为什么那么快
    【ARM 裸机】C 语言 led 驱动
  • 原文地址:https://blog.csdn.net/w425772719/article/details/127663624