Go maps 实践 (Go Blog 翻译)

原文:Go maps in action

引言

哈希表是计算机科学中最有用的数据结构之一。当前许多哈希表的实现,虽然有各自不同的特性,但它们都提供快速的查找、添加、及删除操作。Go 提供了内置的哈希表实现类型,即 map 类型。

声明及初始化

一个 Go 的 map 类型看起来是下面这个样子的:

map[KeyType]ValueType

其中,KeyType 可以是任意可比较的 (comparable, 稍后会介绍) 类型,ValueType 可以是任意类型,包括另外一个 map!

下面的变量 m 表示字符串的键映射到 int 值的 map:

var m map[string]int

Map 类型是引用类型,像指针或切片 (slice) 一样,因此上面 m 的值是 nil;它不指向一个完成初始化的 map。对于一个nil map,从它读取数据的时候,等价于从一个 (初始化好的) 空 map 读取数据;但向它写入数据时,会报运行期错误 (runtime panic),这是需要避免的操作。初始化 map 需要使用内置的 make 函数:

m = make(map[string]int)

make 函数分配内存地址,并在这个地址上初始化一个哈希 map 的数据结构,然后返回指向这个数据结构内存地址的 map 值。这个数据结构的细节是运行期的实现细节,并不是由语言自己确定的。在本文中,我们着重考虑 map 的使用,而不是实现。

操作 maps

Go 操作 map 的语法很简单熟悉。下面操作设置键 “route” 映射到值 66 上:

m["route"] = 66

下面的操作从 map 中获取键为 “route” 对应的值,并把这个值赋给新的变量 i:

i := m["route"]

如果请求的键不存在,那么我们会得到值相应类型的零值。在这个例子里,值的类型是 int,那么零值就是 0:

j := m["root"]
// j == 0

内置的 len 函数返回 map 中键值对的数量:

n := len(m)

内置的 delete 函数删除 map 中的键值对:

delete(m, "route")

delete 函数不会返回任何值。如果要删除的键值对不存在,那么 delete 函数不会做任何事情。

二值赋值操作用于检测 key 是否存在:

i, ok := m["route"]

在这个操作中,i 表示键 “route” 对应的值。如果这个键不存在,那么 i 为 0。ok 是一个 bool 类型的值,表示键是否存在;如果键存在,那么 ok 为 true;如果键不存在,那么 ok 为 false。

如果只想测试键值对不存在,而不获取相应的值,那么可以在第一个返回值处用下划线表示:

_, ok := m["route"]

可以使用 range 关键字来遍历 map 的内容:

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

使用已有数据初始化 map 时,使用以下语法:

commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

也可以用这个语法来初始化空的 map,等价于使用 make 函数:

m = map[string]int{}

利用零值

当键值对不存在时,从 map 读取相应的键,返回零值。这个特性在实际应用中会非常方便:

例如:值为 boolean 类型的 map 可以用来当做类似 set 的数据结构 (boolean 的零值是 false)。下面的例子遍历一个链表,并打印各节点的值。这个程序使用节点的 map 来检测链表中是否有环。

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)
}

如果节点 n 已经被访问过了,那么 visited[n] 返回 true,否则返回 false。在这里我们没必要使用二值类型来检测 n 是否存在于 map 中,而是使用默认的零值来完成相应的功能。

另一个使用零值的例子是 slice 的 map。向一个 nil 的 slice 添加值,会分配一个新的 slice,向 map 里的 slice 添加值也是一样的;我们没必要去检查 slice 是否存在。在下面这个例子中,people 这个 slice 存储的是 Person 类型的值。每个 Person 有一个 Name 和一个 slice 类型的 Likes。下面的例子创建了一个 map 来关联每个爱好和一个存放拥有这个爱好的人的 slice。

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)
    }
}

下面的代码打印所有喜欢奶酪的人:

for _, p := range likes["cheese"] {
    fmt.Println(p.Name, "likes cheese.")
}

下面的代码打印喜欢培根的人的数量:

fmt.Println(len(likes["bacon"]), "people like bacon.")

注意,由于 range 和 len 会把 ni 类型的 slice 当做零长度的 slice 来处理,所以上面两个例子在每人喜欢奶酪或培根的情况下也能正确执行 (但是那不太可能发生 - 作者意思是人人都喜欢奶酪和培根 译者注)。

键的类型

如之前所提,map 的键的类型可以是任意可比较的 (comparable) 类型。Go 语言规范里面对此有详细定义。简而言之,可比较类型有 boolean、numberic、string、指针、channel、以及 interfece,还有只含有以上类型的结构体或数组。值得注意的是,不可比较的类型包括 slice、map、和函数。这些类型不能使用 == 来比较,也不能用作 map 的键。

很明显字符串、整型值、以及其他基础数据类型可以用作 map 的键,但是比较意外的是,结构体也可以作为 map 的键。结构体可以在多维中被使用。例如,下面一个 map 用作匹配与各个国家相符的网页:

hits := make(map[string]map[string]int)

这个 map 从字符串映射到另一个 map (在这个 map 里从字符串映射到整型值)。对于外部的 map,每个 key 表示一个网页的路径。对于内部的 map,每个 key 是一个二个字符的国家码。下面的命令返回澳大利亚人访问文档页的次数:

n := hits["/doc/"]["au"]

当添加数据时,这种方式会显得很笨重,因为对于每个给定的外部键,你需要检查内部键是否存在,并在需要的时候创建它:

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")

下面的设计能简化上面的复杂操作,它使用一个简单的 map,键类型是一个结构体:

type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

当一个越南人访问主页时,可以使用以下操作累加:

hits[Key{"/", "vn"}]++

读取操作也很直观。比如看有多少瑞士人看了规范页面:

n := hits[Key{"/ref/spec", "ch"}]

并发

Map 并不是并发安全的:并没有定义当你并发读写一个 map 时,到底会发生什么。如果你需要在并发的协程中分别读写一个 map,那么对 map 的访问必须使用并发机制来控制。一个常用的保护 map 的方法是使用 sync.RWMutex

下面的例子生命了一个名叫 counter 的变量,类型是一个匿名结构体,包含一个 map 和一个内置的 sync.RWMutex。

var counter = struct {
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

从 counter 读取 map 前,需要先获取读锁:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlokc()
fmt.Println("some_key:", n)

往 counter 的 map 写入之前,需要先获取写锁:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

迭代顺序

使用 range 来遍历 map 时,迭代顺序是不确定的。每次迭代的顺序也不保证一样。自从 Go 1 以来,运行期会随机化 map 的迭代顺序,因为码农们会依赖先前实现的稳定迭代顺序。如果你需要稳定的迭代顺序,你必须适应一个额外的数据结构来指明顺序。下面的例子使用一个额外的键的有序切片来按照键的顺序打印 map:

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])
}

-- 作者:Andrew Gerrand

转载于:https://my.oschina.net/dokia/blog/1841458

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值