Go map 实战教程

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

Andrew Gerrand
2013年2月6日

简介

hash map 是计算机科学领域中最常用的数据结构之一。 许多 hash map 实现具有各种各样的特性, 但通常他们都有快速查找、添加和删除元素的功能. Go 提供了内置的 hash map.

声明和初始化

Go 的map类型长这样:

map[KeyType]ValueType

它的 KeyType 可以是任意可比较的 类型(稍后会进行详述),而 ValueType 可以是任意类型, 甚至可以是另一个 map!

变量 m 是一个map,它的键是 string 类型,值是 int 类型:

var m map[string]int

map 类型是引用类型, 就像指针或者切片, 因此上面的 m 的值是 nil; 它没有指向一个已初始化的 map. 读取一个值为 nil 的 map 就像读取一个空的 map 一样, 但如果尝试向一个值为 nil 的 map 写入数据时,就会产生运行时 panic; 请不要这么做。 可以使用内置的 make 函数去初始化一个 map:

m = make(map[string]int)

make 函数为 hash map 数据结构分配空间,并初始化它,然后返回一个指向它的 map 值. 这个数据结构的特性是运行时的实现细节,它不由语言本身规定。本文只关注 map 的使用,而非他们的实现。

使用 map

Go 提供了使用 map 的一种常见的语法。 这条语句设置键 "route",映射到值 66:

m["route"] = 66

这条语句检索键"route"对应的值并且把它赋值给一个新的变量 i:

i := m["route"]

如果要查找的键不存在,我们会得到值的类型的零值. 在这个例子中,值的类型为 int, 因此零值是0:

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

内置的 len 函数返回一个 map 中的元素个数:

n := len(m)

内置的 delete 函数从 map 中移除一个键值对:

delete(m, "route")

delete 函数不会返回任何值,并且如果指定的键不存在时,它将什么也不做。

使用两个变量接收查询结果,来测试一个键是否存在:

i, ok := m["route"]

在这条语句中,第一个变量 (i) 会接收键"route"对应的值。如果该键不存在, i 就是这个值的类型的零值 (0). 第二个变量 (ok) 是 bool 变量,如果 map 中有该键,它就为 true否则,它为 false

如果仅仅为了测试某个键是否存在,而不需要检索它对应的值, 可以用下划线_替换第一个变量:

_, ok := m["route"]

使用 range 关键字去迭代 map 的内容:

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

使用 map 字面量去初始化一个包含一些数据的 map:

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

可以使用相同的语法去初始化一个空的 map, 这么做和使用 make 函数的方式等价:

m = map[string]int{}

利用零值

当键不存在时,map检索会产生零值,这有时是很方便的。

例如,布尔值映射可以用作类似集合的数据结构(请注意,布尔类型的零值为false)。此示例遍历Nodes的链接列表并打印其值。它使用Node指针的映射来检测列表中的循环。

    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,否则如果n不存在,则为 false 。无需使用二值形式来测试地图中n的存在;零值默认值对我们有用。

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

Another instance of helpful zero values is a map of slices. Appending to a nil slice just allocates a new slice, so it's a one-liner to append a value to a map of slices; there's no need to check if the key exists. In the following example, the slice people is populated with Person values. Each Person has a Name and a slice of Likes. The example creates a map to associate each like with a slice of people that like it.

    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都将nil切片视为零长度切片,因此即使没有人喜欢奶酪或培根(尽管不太可能),后两个示例也可以使用。

键类型

如前所述, map 的键可以是任何可比较的类型。 语言规范 对此进行了精确定义, 但简而言之, 可比较的类型是布尔, 数字, 字符串, 指针, 通道和接口类型, 以及可用于仅包含那些类型. 切片, 集合和函数显然不在此列表中;这些类型不能使用 == 进行比较, 并且不能用作集合键.

显然, 字符串, 整数和其他基本类型都可以用作映射键, 但是结构键可能出乎意料. 结构可以用于多维维度的关键数据. 例如, 此集合可以用于按国家/地区统计网页点击量:

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

这是字符串到 (stringint 的集合) 的集合. 外部集合的每个键都是具有内部集合的网页路径. 每个内部集合键是一个两个字母的国家/地区代码. 此表达式检索澳大利亚人加载文档页面的次数:

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

另一方面, 使用带有结构键的单个集合的设计可以消除所有的复杂性:

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

当越南人访问主页时, 增加 (并可能创建) 适当的计数器是一种情况:

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

看看有多少瑞士人已经阅读了该规范, 同样也很简单:

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

并发

并发使用 Map 并不安全: 同时定义读写时发生的情况并不确定. 如果您需要从并发执行的 goroutine 中读取和写入 map, 则必须通过某种同步机制来调解访问. 保护 map 的一种常见方法是 sync.RWMutex.

该语句声明一个 counter 变量, 该变量是一个匿名结构, 包含一个 map 和嵌入的 sync.RWMutex.
This statement declares a counter variable that is an anonymous struct containing a map and an embedded sync.RWMutex.

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

要从计数器读取, 请使用读取锁:

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

要写入计数器, 请使用写锁:

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

迭代顺序

当使用范围循环在地图上进行迭代时, 未指定迭代顺序, 并且不能保证每次迭代之间都相同. 如果需要稳定的迭代顺序, 则必须维护一个指定该顺序的单独的数据结构. 本示例使用单独的键排序片来按键顺序打印 map[int]string:

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

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/go-maps...

译文地址:https://learnku.com/docs/go-blog/go-maps...

上一篇 下一篇
Summer
贡献者:3
讨论数量: 0
发起讨论 只看当前版本


暂无话题~