写点什么

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

作者:王中阳Go
  • 2022-11-03
    北京
  • 本文字数:5054 字

    阅读完需:约 17 分钟

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)
复制代码

实践得真知

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. 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: linuxgoarch: amd64Benchmark_GMapSet-4                     10000000               209 ns/op              15 B/op          0 allocs/opBenchmark_SyncMapSet-4                   3000000               451 ns/op              67 B/op          3 allocs/opBenchmark_GMapGet-4                     30000000              66.4 ns/op               0 B/op          0 allocs/opBenchmark_SyncMapGet-4                  30000000              36.0 ns/op               0 B/op          0 allocs/opBenchmark_GMapRemove-4                  10000000               207 ns/op               0 B/op          0 allocs/opBenchmark_SyncMapRmove-4                30000000              42.4 ns/op               0 B/op          0 allocs/op
复制代码


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

基础使用

  1. gmap.New(true) 在初始化的时候开启并发安全开关

  2. 通过 Set() 方法赋值,通过 Sets() 方法批量赋值

  3. 通过 Size() 方法获取 map 大小

  4. 通过 Get() 根据 key 获取 value 值

  5. ...


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


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


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())}
复制代码

运行结果


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

合并 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]}
复制代码

打印结果

序列化

正如之前的文章 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:王中阳]}
复制代码

打印结果

踩坑

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


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


m := gmap.New() 
复制代码



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


var m gmap.Map
复制代码



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

过滤空值

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}}
复制代码

打印结果


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

键值对反转 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())}
复制代码

打印结果

出栈(随机出栈)

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


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()) //多次测试之后发现也是随机出栈}
复制代码

运行结果

踩坑

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


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

总结

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


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

  2. 当我们对返回顺序有要求时不能使用HashMap,因为HashMap返回的是无序列表;

  3. 当需要按输入顺序返回结果时使用ListMap

  4. 当需要让返回结果按照自然升序排列时使用TreeMap

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

  6. gmap 的基础使用和进阶使用技巧:反转 map、序列化、合并 map、出栈等。

  7. gf 框架提供的数据结构,比如:字典 gmap、数组 garray、集合 gset、队列 gqueue、树形结构 gtree、链表 glist 都是支持设置并发安全开关的;而且都支持序列化和反序列化,实现了标准库json数据格式的序列化/反序列化接口。

一起学习

公众号:程序员升级打怪之旅


微信号:wangzhongyang1993


B 站视频:https://space.bilibili.com/19626338

发布于: 2022-11-03阅读数: 72
用户头像

王中阳Go

关注

公众号:程序员升级打怪之旅 2022-10-09 加入

微信:wangzhongyang1993

评论 (1 条评论)

发布
用户头像
有好多初学 GO 和 GoFrame 的小伙伴搞不清楚 map 怎么用。
不少刚入门的小伙伴都被 Go 语言中 map 的无序性“坑过”,尤其是 PHP 转 Go 的小伙伴,毕竟用惯了 PHP 的数组。
2022-11-03 09:54 · 北京
回复
没有更多了
GoFrame的gmap相比Go原生的map,天然支持排序和有序遍历!?_Go_王中阳Go_InfoQ写作社区