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
发起讨论 查看所有版本


暂无话题~