Go语言map实践

1,079 阅读7分钟
原文链接: mp.weixin.qq.com
计算机科学中最有用的数据结构是哈希表,许多哈希表的实现包含多种属性,但总的来说基本包含快速查找、添加和删除。Go语言的内建类型——映射(map)就通过哈希表实现。

声明和初始化

Go语言中的映射类型看起来像这样:

map [KeyType] ValueType

KeyType可以是任意可比较的类型 、ValueType可以是任意类型,甚至是一个其他的映射。如下m为映射类型的变量,Key是string类型的,Value是int类型:

var m map[string]int

映射是引用类型,类似于指针或切片(slice),所以上面m的是值是nil,并没有被初始化;读取nil 映射同读取空映射一样,但对nil映射进行写操作时会导致运行时错误,所以不要像上面那样初始化map,需要使用make函数:

m = make(map[string]int)

make函数会分配和初始化一个哈希映射数据结构,并返回一个指向该哈希映射的映射变量。相关数据结构细节由运行时实现,并且不因语言本身所规定。本文中我们将关注映射的使用,而不是他们的实现。

映射的使用

Go语言提供常见的映射使用语法,下面的声明是将键“route”赋值为66

m[“route”] = 66

这个声明是将值存储在键“route”下,返回并分配给新的变量i:

i := m[“route”]

如果返回的键不存在,我们得到值类型对应的零值 ;这个例子中值的类型是int类型,所以零值是0:

j := m[“root”]

// j == 0

len函数可以返回映射中条目的数量:

n := len(m)

delete函数可以删除映射中的一个条目:

delete(m, “route”)

delete函数不会返回任何值,如果特定的键不存在,那么将不会执行任何操作。

给一个已存在的键进行二值分配测试:

i, ok := m[“route”]

在这种情况下,第一个值(i)用于存储键“route”对应的值。如果这个键不存在,i的值是该类型的零值(0)。第二个值(ok)是布尔类型的值,如果被测试的键在映射中存在返回true,否则返回false。

如果不返回键的值,则第一个值使用下划线表示:

_, ok := m[“route”]

使用range关键字遍历映射中的内容:

for key, value := range m {

    fmt.println(“Key:”, key, “Value:”, value)

}

使用映射字面量进行初始化::

commits := map [string] int {

    “rsc” : 3711,

    “r” : 2138,

    “gri” : 1908,

    “adg” : 912,

}

 

这种方法同样可以初始化一个空的映射,此时等价于make函数的功能相同:

m = map[string] int { }

零值的使用

当键不存在时,映射返回零值,这样处理是相当方便的。

像下面这个例子,一个值为布尔型的映射可以被用作集合来使用(布尔类型的零值为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,如果不存在,则返回false。在映射中没有必要使用2个值的形式去检测n是否存在;对于我们来说零值默认的。

另一个例子,展示了在切片类型的映射中零值的好处。向一个nil切片进行追加会创建一个新的slice,这等同于给切片类型的映射增加一个值;这种情况没有必要检查键是否存在。在接下来的例子中,切片people 被Person类型的值填充。每一个Person都有一个Name和一系列Likes。这个例子创建一个映射,将每个like的值 与一组喜欢它的people关联起来。

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)

     }

}

打印那些like cheese 的people列表:

for _, p := range likes["cheese"] {

    fmt.Println(p.Name, "likes cheese.")

}

打印like bacon的people的数量:

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

需要注意的是,对于空的切片,关键字range和len返回的都是长度0. 最后两个例子即使没有人喜欢 cheese 或者 bacon,程序也会被执行(无论不存在那种可能)

键的类型

在前文提到过映射的键可能是可比较的任意类型。Go语言规范有明确的定义,简单说,可比较的类型包含布尔类型、数值型、字符串、指针、管道、接口类型、结构体、数组这些类型。很明显少了列表、切片、映射和函数,这些类型不能使用==进行比较,不能作为映射的键使用。

很明显字符串、整型以及其他基础类型可以作为映射的键,但可能存在意想不到的结构体键,结构体可以被适用于多维度的键数据。例如,映射类型的maps可以被用于统计对应国家的网页点击率:

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

这段代码代表了键为string类型,值为“键为string类型,值为int类型的映射”的映射。外层映射的每个键是网页本身的映射的路径。每个内在映射的键为2位国家代码。下面的代码表示了在澳大利亚,/doc/这个文件页被点击的次数。

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

并发

映射不是并发安全的:映射并没有定义当同时进行读和写时会发生什么。如果你需要执行同时对映射进行读取和写入操作的goroutines时,这种访问必须使用某种同步机制,比如sync.RWMutex.

这个例子声明了一个匿名结构体类型的counter变量,这个结构体包含了一个映射以及一个被嵌入的sync.RWMutex。

var counter = struct{

sync.RWMutex

m map[string]int

}{m: make(map[string]int)}

从counter中执行带锁的读操作:

counter.RLock()

n := counter.m["some_key"]

counter.RUnlock()

fmt.Println("some_key:", n)

从counter中执行带锁的写操作:

counter.Lock()

counter.m["some_key"]++

counter.Unlock()

迭代顺序

当使用range循环遍历一个映射时,遍历顺序并不是确定的,不能保证下次遍历的结果与这次的结果相同。从Go 1版本开始,对映射遍历顺序进行了随机化处理。

如果你需要一个固定的遍历过程,你必须去维护一个明确且独立的数据结构。下面的例子中使用单独的排序过的切片作为键,并以此为依据中打印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])

}

参考资料

原文链接:https://blog.golang.org/go-maps-in-action

原文作者:Andrew Gerrand

Ku beCon 上海

KubeCon CloudNativeCon将于2018年11月14日—15日登陆中国上海跨国采购会展中心,现已开启讲稿征集 ,欲知详情请点击下方阅读原文 (如打开较慢,请点击右上角使用浏览器打开)。