深圳幻海软件技术有限公司 欢迎您!

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

2023-02-28

前言有好多初学GO的小伙伴都被Go语言中map的无序性“坑过”。尤其是PHP转Go的小伙伴~这篇文章会为大家介绍:GoFrame的gmap相比于Go原生的map有什么优势?为什么天然支持排序和有序遍历?如何做到的?GoFrame的gmap有哪些使用技巧?先说结论GoFrame提供的gmap字典类型,

前言

有好多初学GO的小伙伴都被Go语言中map的无序性“坑过”。尤其是PHP转Go的小伙伴~

这篇文章会为大家介绍:

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

GoFrame的gmap有哪些使用技巧?

先说结论

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

使用技巧

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

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

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

一图胜千言

GoFrame gmap 基本介绍:

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

该模块包含多个数据结构的map​容器:HashMap、TreeMap和ListMap。

实例化示例:

   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.

QQyIjR" id="hce1b53f-emQQyIjR">打印结果

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

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

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

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.

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

基础使用

  • gmap.New(true) 在初始化的时候开启并发安全开关
  • 通过 Set() 方法赋值,通过 Sets() 方法批量赋值
  • 通过 Size() 方法获取map大小
  • 通过 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 基础使用和自定义遍历[3] 介绍的,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.

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

过滤空值

首先明确:空值和nil是不一样的。

nil是未定义;而空值包括空字符串,false、0等

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,其他都返回
}
  • 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.

打印结果

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

键值对反转 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.Map对象的底层实现是hashmap,本身就是无序的,当然不可能按顺序出栈了。

总结

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

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

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

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

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

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

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

相关文章

[1]# Go容易搞错的知识点汇总:Go map如何实现排序 部分: https://juejin.cn/post/7131717990558466062#heading-25

[2]官方文档的介绍: https://goframe.org/pages/viewpage.action?pageId=30736719

[3]GoFrame glist 基础使用和自定义遍历: https://juejin.cn/post/7101515355062796296

本文转载自微信公众号「 程序员升级打怪之旅」,作者「王中阳Go」,可以通过以下二维码关注。

转载本文请联系「 程序员升级打怪之旅」公众号。