字典在数学上的词汇是映射,将一个集合中的所有元素关联到另一个集合中的部分或全部元素,并且只能是一一映射或者多对一映射。

第 6 课 —— 字典 - 图1

数组切片让我们具备了可以操作一块连续内存的能力,它是对同质元素的统一管理。而字典则赋予了不连续不同类的内存变量的关联性,它表达的是一种因果关系,字典的 key 是因,字典的 value 是果。如果说数组和切片赋予了我们步行的能力,那么字典则让我们具备了跳跃的能力。

指针、数组切片和字典都是容器型变量,字典比数组切片在使用上要简单很多,但是内部结构却无比复杂。本节我们只专注字典的基础使用,在后续的高级章节再来分析它的内部结构。

字典的创建

关于 Go 语言有很多批评的声音,比如说它不支持范型。其实严格来说 Go 是支持范型的,只不过很弱,范型在 Go 语言里是一种很弱的存在。比如数组切片和字典类型都是支持范型的。在创建字典时,必须要给 key 和 value 指定类型。创建字典也可以使用 make 函数

  1. package main
  2. import "fmt"
  3. func main() {
  4. var m map[int]string = make(map[int]string)
  5. fmt.Println(m, len(m))
  6. }
  7. ----------
  8. map[] 0

使用 make 函数创建的字典是空的,长度为零,内部没有任何元素。如果需要给字典提供初始化的元素,就需要使用另一种创建字典的方式。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var m map[int]string = map[int]string{
  5. 90: "优秀",
  6. 80: "良好",
  7. 60: "及格", // 注意这里逗号不可缺少,否则会报语法错误
  8. }
  9. fmt.Println(m, len(m))
  10. }
  11. ---------------
  12. map[90:优秀 80:良好 60:及格] 3

字典变量同样支持类型推导,上面的变量定义可以简写成

  1. var m = map[int]string{
  2. 90: "优秀",
  3. 80: "良好",
  4. 60: "及格",
  5. }

如果你可以预知字典内部键值对的数量,那么还可以给 make 函数传递一个整数值,通知运行时提前分配好相应的内存。这样可以避免字典在长大的过程中要经历的多次扩容操作。

  1. var m = make(map[int]string, 16)

字典的读写

同 Python 语言一样,字典可以使用中括号来读写内部元素,使用 delete 函数来删除元素。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var fruits = map[string]int {
  5. "apple": 2,
  6. "banana": 5,
  7. "orange": 8,
  8. }
  9. // 读取元素
  10. var score = fruits["banana"]
  11. fmt.Println(score)
  12. // 增加或修改元素
  13. fruits["pear"] = 3
  14. fmt.Println(fruits)
  15. // 删除元素
  16. delete(fruits, "pear")
  17. fmt.Println(fruits)
  18. }
  19. -----------------------
  20. 5
  21. map[apple:2 banana:5 orange:8 pear:3]
  22. map[orange:8 apple:2 banana:5]

字典 key 不存在会怎样?

删除操作时,如果对应的 key 不存在,delete 函数会静默处理。遗憾的是 delete 函数没有返回值,你无法直接得到 delete 操作是否真的删除了某个元素。你需要通过长度信息或者提前尝试读取 key 对应的 value 来得知。

读操作时,如果 key 不存在,也不会抛出异常。它会返回 value 类型对应的零值。如果是字符串,对应的零值是空串,如果是整数,对应的零值是 0,如果是布尔型,对应的零值是 false。

你不能通过返回的结果是否是零值来判断对应的 key 是否存在,因为 key 对应的 value 值可能恰好就是零值,比如下面的字典你就不能判断 “durin” 是否存在

  1. var m = map[string]int {
  2. "durin": 0 // 举个栗子而已,其实我还是喜欢吃榴莲的
  3. }

这时候必须使用字典的特殊语法,如下

  1. package main
  2. import "fmt"
  3. func main() {
  4. var fruits = map[string]int {
  5. "apple": 2,
  6. "banana": 5,
  7. "orange": 8,
  8. }
  9. var score, ok = fruits["durin"]
  10. if ok {
  11. fmt.Println(score)
  12. } else {
  13. fmt.Println("durin not exists")
  14. }
  15. fruits["durin"] = 0
  16. score, ok = fruits["durin"]
  17. if ok {
  18. fmt.Println(score)
  19. } else {
  20. fmt.Println("durin still not exists")
  21. }
  22. }
  23. -------------
  24. durin not exists
  25. 0

字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在。初学者看到这种奇怪的用法是需要花时间来消化的,读者不需要想太多,它只是 Go 语言提供的语法糖,内部并没有太多的玄妙。正常的函数调用可以返回多个值,但是并不具备这种“随机应变”的特殊能力 —— 「多态返回值」。

字典的遍历

字典的遍历提供了下面两种方式,一种是需要携带 value,另一种是只需要 key,需要使用到 Go 语言的 range 关键字。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var fruits = map[string]int {
  5. "apple": 2,
  6. "banana": 5,
  7. "orange": 8,
  8. }
  9. for name, score := range fruits {
  10. fmt.Println(name, score)
  11. }
  12. for name := range fruits {
  13. fmt.Println(name)
  14. }
  15. }
  16. ------------
  17. orange 8
  18. apple 2
  19. banana 5
  20. apple
  21. banana
  22. orange

奇怪的是,Go 语言的字典没有提供诸于 keys() 和 values() 这样的方法,意味着如果你要获取 key 列表,就得自己循环一下,如下

  1. package main
  2. import "fmt"
  3. func main() {
  4. var fruits = map[string]int {
  5. "apple": 2,
  6. "banana": 5,
  7. "orange": 8,
  8. }
  9. var names = make([]string, 0, len(fruits))
  10. var scores = make([]int, 0, len(fruits))
  11. for name, score := range fruits {
  12. names = append(names, name)
  13. scores = append(scores, score)
  14. }
  15. fmt.Println(names, scores)
  16. }
  17. ----------
  18. [apple banana orange] [2 5 8]

这会让代码写起来比较繁琐,不过 Go 语言官方就是没有提供,读者还是努力习惯一下吧

线程(协程)安全

Go 语言的内置字典不是线程安全的,如果需要线程安全,必须使用锁来控制。在后续锁的章节里,我们将会自己实现一个线程安全的字典。

字典变量里存的是什么?

字典变量里存的只是一个地址指针,这个指针指向字典的头部对象。所以字典变量占用的空间是一个字,也就是一个指针的大小,64 位机器是 8 字节,32 位机器是 4 字节。

第 6 课 —— 字典 - 图2

可以使用 unsafe 包提供的 Sizeof 函数来计算一个变量的大小

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func main() {
  7. var m = map[string]int{
  8. "apple": 2,
  9. "pear": 3,
  10. "banana": 5,
  11. }
  12. fmt.Println(unsafe.Sizeof(m))
  13. }
  14. ------
  15. 8

思考题

在遍历字典得到 keys 和 values 的例子里,我们分配了 names 和 scores 两个切片,如果把代码片断调整成下面这样,会有什么问题?

  1. var names = make([]string, len(fruits))
  2. var scores = make([]int, len(fruits))

第 6 课 —— 字典 - 图3

阅读《快学 Go 语言》更多章节,用微信扫一扫上面的二维码关注公众号「码洞」