前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go maps in action

Go maps in action

作者头像
孟斯特
发布2024-01-29 14:45:55
1440
发布2024-01-29 14:45:55
举报
文章被收录于专栏:code人生code人生

原文在这里[1]。

由 Andrew Gerrand 发布于2013年2月6日

简介

计算机科学中最有用的数据结构之一是哈希表。尽管存在许多不同属性的哈希表实现,但总体上它们提供了快速的查找、添加和删除操作。Go提供了一种内置的map类型,它实现了一个哈希表。

声明与初始化

Go中的map类型如下所示:

代码语言:javascript
复制
map[KeyType]ValueType

其中KeyType可以是任何可比较的类型[2](稍后详细介绍),而ValueType可以是任何类型,甚至可以是另一个map

这个变量m是一个从字符串键到整数值的映射:

代码语言:javascript
复制
var m map[string]int

映射类型是引用类型,类似于指针或切片,因此上述的m的值是nil;它并未指向一个初始化的映射。当读取时,nil映射的行为类似于空映射,但尝试向nil映射写入会导致运行时错误;所以应该避免向nil映射写入数据。要初始化映射,请使用内置的make函数:

代码语言:javascript
复制
m = make(map[string]int)

make函数会分配并初始化一个哈希映射数据结构,并返回指向它的映射值。该数据结构的具体细节是运行时的实现细节,不由语言本身规定。在本文中,我们将专注于映射的使用,而不是它们的实现。

使用maps

Go为处理映射提供了便捷的语法。以下语句将键"route"设置为值66:

代码语言:javascript
复制
m["route"] = 66

下面我们检索下route的值并赋值给变量i

代码语言:javascript
复制
i := m["route"]

如果检索的key不存在,将会放回该变量类型的

零值

。在我们的使用场景中因为变量的类型是int,所以它的零值是0

代码语言:javascript
复制
j := m["root"]
// j == 0

内建的len函数可以返回map中的元素个数:

代码语言:javascript
复制
n := len(m)

内建的delete函数可以删除map中的元素:

代码语言:javascript
复制
delete(m, "route")

delete函数并不会返回任何值,所以即使指定的key不存在也不会有任何反应。

一个双值赋值可以测试一个键是否存在:

代码语言:javascript
复制
i, ok := m["route"]

在这个语句中,第一个值(i)被赋予键"route"下存储的值。如果该键不存在,i将是值类型的零值(0)。第二个值(ok)是一个布尔值,如果键存在于map中,则为true,否则为false

如果只是测试key是否存在,那可以在第一个变量的位置使用下划线:

代码语言:javascript
复制
_, ok := m["route"]

要迭代地遍历map的内容,可以使用range关键字:

代码语言:javascript
复制
for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

要使用一些数据初始化map,可以逐个赋值:

代码语言:javascript
复制
commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

相同的语法可用于初始化空映射,从功能上讲,它与使用make函数相同:

代码语言:javascript
复制
m = map[string]int{}

利用零值

检索map时,如果键不存在,得到零值可能很方便。

例如,map的布尔值可以用作类似集合的数据结构(回想一下布尔类型的零值是false)。此示例遍历Nodes链表并打印其值。它使用节点指针的map来检测列表中的循环。

代码语言:javascript
复制
type Node struct {
    Next  *Node
    Value interface{}
}
var first *Node

visited := make(map[*Node]bool)
for n := first; n != nil; n = n.Next {
    if visited[n] {
        fmt.Println("cycle detected")
        break
    }
    visited[n] = true
    fmt.Println(n.Value)
}

表达式visited[n]如果n已被访问则为true,如果n不在map中则为false。我们无需使用两值形式来检查map中是否存在n;零值默认会为我们执行此操作。

另一个有用的零值示例是切片的map。将值附加到nil切片只会分配一个新切片,因此将值附加到切片的map是一种简便方法;无需检查键是否存在。在以下示例中,切片people填充了Person值。每个Person都有一个Name和一个Likes切片。该示例创建了一个map,将每个喜欢与一个喜欢它的人的切片关联起来。

代码语言:javascript
复制
type Person struct {
    Name  string
    Likes []string
}
var people []*Person

likes := make(map[string][]*Person)
for _, p := range people {
    for _, l := range p.Likes {
        likes[l] = append(likes[l], p)
    }
}

打印喜欢奶酪的人:

代码语言:javascript
复制
for _, p := range likes["cheese"] {
    fmt.Println(p.Name, "likes cheese.")
}

打印喜欢熏肉的人数:

代码语言:javascript
复制
fmt.Println(len(likes["bacon"]), "people like bacon.")

需要注意的是,由于rangelen都将nil切片视为零长度切片,因此即使没有人喜欢奶酪或培根(尽管可能性微乎其微),上述最后两个示例也将正常工作。

键类型

如前所述,map的键可以是任何可比较的类型。语言规范[3]对此进行了明确定义,但简而言之,可比较的类型包括布尔、数字、字符串、指针、通道和接口类型,以及仅包含这些类型的结构体或数组。值得注意的是,切片、map和函数不在列表中;这些类型不能使用==进行比较,也不能用作map键。

字符串、整数和其他基本类型应该作为map键,出人意料的是结构体也可以作为map的键。结构体可用于通过多个维度对数据进行键控。例如,下面的map可以用于按国家统计网页点击次数:

代码语言:javascript
复制
hits := make(map[string]map[string]int)

这是一个字符串-(字符串-int)的映射。外部map的每个键都对应于一个内部的map,存储着网页路径。每个内部map键是一个两字母的国家代码。此表达式检索加载文档页面的澳大利亚用户的次数:

代码语言:javascript
复制
n := hits["/doc/"]["au"]

不幸的是,当添加数据时,这种方法变得笨拙,因为对于任何给定的外部键,都必须检查内部map是否存在,并在需要时创建它:

代码语言:javascript
复制
func add(m map[string]map[string]int, path, country string) {
    mm, ok := m[path]
    if !ok {
        mm = make(map[string]int)
        m[path] = mm
    }
    mm[country]++
}
add(hits, "/doc/", "au")

但使用具有结构键的单个映射的设计摆脱了所有这些复杂性:

代码语言:javascript
复制
type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

当越南人访问主页时,递增(可能是创建)适当的计数器只需一行代码:

代码语言:javascript
复制
hits[Key{"/", "vn"}]++

现在简单地看到多少瑞士人阅读了规范:

代码语言:javascript
复制
n := hits[Key{"/ref/spec", "ch"}]

并发

map并不是并发安全的[4]:同时读写时发生的事情并没有定义。如果需要从并发执行的逻辑线程中读写map,则必须通过某种同步机制来管理这些访问。保护map的一种常用方法是使用sync.RWMutex[5]。

下面声明了一个计数器变量,它是一个包含map和嵌入的sync.RWMutex的匿名结构。

代码语言:javascript
复制
var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

从计数器中读取时,需要对读操作进行加锁:

代码语言:javascript
复制
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

向计数器中写入数据时,需要对写操作进行加锁:

代码语言:javascript
复制
counter.Lock()
counter.m["some_key"]++
counter.Unlock()

迭代顺序

在使用range循环迭代映射时,迭代顺序未指定,且不保证从每次迭代的结果都相同。如果需要稳定的迭代顺序,必须维护一个单独的数据结构来指定该顺序。以下示例使用一个单独的按键排序的切片,以按键顺序打印map[int]string

代码语言:javascript
复制
import "sort"

var m map[int]string
var keys []int
for k := range m {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", m[k])
}

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)[6]进行许可,使用时请注明出处。 Author: mengbin[7] blog: mengbin[8] Github: mengbin92[9] cnblogs: 恋水无意[10] 腾讯云开发者社区:孟斯特[11]

References

[1] 这里: https://go.dev/blog/maps [2] 比较的类型: https://go.dev/ref/spec#Comparison_operators [3] 语言规范: https://go.dev/ref/spec#Comparison_operators [4] map并不是并发安全的: https://go.dev/doc/faq#atomic_maps [5] sync.RWMutex: https://go.dev/pkg/sync/#RWMutex [6] 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0): https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh [7] mengbin: mengbin1992@outlook.com [8] mengbin: https://mengbin.top [9] mengbin92: https://mengbin92.github.io/ [10] 恋水无意: https://www.cnblogs.com/lianshuiwuyi/ [11] 孟斯特: https://cloud.tencent.com/developer/user/6649301

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟斯特 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 声明与初始化
  • 使用maps
  • 利用零值
  • 键类型
  • 并发
  • 迭代顺序
    • References
    相关产品与服务
    云开发 CloudBase
    云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档