Go - 6.字典

目录

一.引言

二.字典

1.基础定义

2.映射 - 键值转哈希值

3.键值的限制

三.扩展

1.适合字典键的类型?

2.可以在值为 nil 的字典上读写吗?

四.总结


一.引言

截止目前,我们学习到的数组、切片、链表都是针对单一元素的 container,它们或用连续存储、或用互存指针的方式收纳元素,这里每个元素都代表了一个从属某一类型的独立值。今天介绍的字典 map 与它们不同,字典存储的是键值对的集合。

二.字典

1.基础定义

字典存储的不是单一值的集合,而是键值对的集合。

在 Go 语言规范中,应该是为了避免歧义,他们将键值对换了一种称呼,叫做:“键 - 元素对”。我们也沿用这个看起来更加清晰的词来讲解,下面创建一个 map 并使用使用 k 索引得到 v:

package main

import "fmt"

func main() {

	aMap := map[string]int{
		"one":   1,
		"two":   2,
		"three": 3,
	}
	k := "two"
	v, ok := aMap[k]
	if ok {
		fmt.Printf("The element of key %q: %d\n", k, v)
	} else {
		fmt.Println("Not found!")
	}

}

2.映射 - 键值转哈希值

我们了解下哈希表中最重要的一个过程:映射。你可以把键理解为元素的一个索引,我们可以在哈希表中通过键查找与它成对的那个元素。键和元素的这种对应关系,在数学里就被称为“映射”,这也是 “map” 这个词的本意,哈希表的映射过程就存在于对键 - 元素对的增、删、改、查的操作之中。

	aMap := map[string]int{
		"one":   1,
		"two":   2,
		"three": 3,
	}
	k := "two"
	v, ok := aMap[k]

👆🏻 是一个基础的映射操作,我们要在哈希表中查找与某个键值对应的那个元素值,那么我们需要先把键值作为参数传给这个哈希表。

哈希表会先 用哈希函数(hash function)把键值转换为哈希值。哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的桶(bucket),我们也可以叫它哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的键 - 元素对。

因此,哈希表会先用这个键哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。随后,哈希表就会把相应的元素值作为结果返回。只要这个键 - 元素对存在哈希表中就一定会被查找到,因为哈希表增、改、删键 - 元素对时的映射过程,与前文所述如出一辙。

3.键值的限制

Go 语言的字典类型其实是一个哈希表 (hash table) 的特定实现,在这个实现中,键和元素的最大不同时,键的类型是受限的,而元素可以是任意类型。

通过上面的分析我们知道了,映射过程的第一步就是:把键值转换为哈希值。在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈希值。其中以下类型不支持作为 map 的键:

- 函数类型

- 字典类型

- 切片类型

Go 语言规范规定,在键类型的值之间必须可以施加操作符 == 和 !=。换句话说,键类型的值必须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键类型不能是这些类型。另外,如果键的类型是接口类型的,那么键值的实际类型也不能是上述三种类型,否则在程序运行过程中会引发 panic(即运行时恐慌)。

- 接口类型

var badMap2 = map[interface{}]int{
  "1":   1,
  []int{2}: 2, // 这里会引发panic。
  3:    3,
}

这里的变量 badMap2 的类型是键类型为 interface{}、值类型为 int 的字典类型。这样声明并不会引起什么错误。或者说,我通过这样的声明躲过了 Go 语言编译器的检查。注意,我用字面量在声明该字典的同时对它进行了初始化,使它包含了三个键 - 元素对。其中第二个键 - 元素对的键值是[]int{2},元素值是 2。这样的键值也不会让 Go 语言编译器报错,因为从语法上说,这样做是可以的。但是,当我们运行这段代码的时候,Go 语言的运行时(runtime)系统就会发现这里的问题,它会抛出一个 panic,并把根源指向字面量中定义第二个键 - 元素对的那一行。我们越晚发现问题,修正问题的成本就会越高,所以最好不要把字典的键类型设定为任何接口类型。如果非要这么做,请一定确保代码在可控的范围之内。

还要注意,如果键的类型是数组类型,那么还要确保该类型的元素类型不是函数类型、字典类型或切片类型。比如,由于类型 [1][]string 的元素类型是 []string,所以它就不能作为字典类型的键类型。另外,如果键的类型是结构体类型,那么还要保证其中字段的类型的合法性。无论不合法的类型被埋藏得有多深,比如 map[[1][2][3][]string]int,Go 语言编译器都会把它揪出来。

- 为什么要支持判等操作

首先,每个哈希桶都会把自己包含的所有键的哈希值存起来。Go 语言会用被查找键的哈希值与这些哈希值逐个对比,看看是否有相等的。

        如果一个相等的都没有,那么就说明这个桶中没有要查找的键值,这时 Go 语言就会立刻返回结果了。

        如果有相等的,那就再用键值本身去对比一次。

为什么还要对比?原因是,不同值的哈希值是可能相同的。这有个术语,叫做 “哈希碰撞”。所以,即使哈希值一样,键值也不一定一样。如果键类型的值之间无法判断相等,那么此时这个映射的过程就没办法继续下去了。最后,只有键的哈希值和键值都相等,才能说明查找到了匹配的键 - 元素对。

三.扩展

1.适合字典键的类型?

这里先抛开我们使用字典时的上下文,只从性能的角度看。在前文所述的映射过程中,“把键值转换为哈希值” 以及 “把要查找的键值与哈希桶中的键值做对比”, 明显是两个重要且比较耗时的操作。

因此,可以说,求哈希和判等操作的速度越快,对应的类型就越适合作为键类型。对于所有的基本类型、指针类型,以及数组类型、结构体类型和接口类型,Go 语言都有一套算法与之对应。这套算法中就包含了哈希和判等。以求哈希的操作为例,宽度越小的类型速度通常越快。对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。

类型的宽度是指它的单个值需要占用的字节数。比如,bool、int8 和 uint8 类型的一个值需要占用的字节数都是 1,因此这些类型的宽度就都是 1。

以上说的都是基本类型,再来看高级类型。对数组类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度。细则同上。与之类似,对结构体类型的值求哈希实际上就是对它的所有字段值求哈希并进行合并,所以关键在于它的各个字段的类型以及字段的数量。而对于接口类型,具体的哈希算法,则由值的实际类型决定。

这里不建议你使用这些高级数据类型作为字典的键类型,不仅仅是因为对它们的值求哈希,以及判等的速度较慢,更是因为在它们的值中存在变数。比如,对一个数组来说,我可以任意改变其中的元素值,但在变化前后,它却代表了两个不同的键值。

那么,在那些基本类型中应该优先选择哪一个?答案是,优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。那什么是不通常的情况?笼统地说,Go 语言有时会对字典的增、删、改、查操作做一些优化。比如,在字典的键类型为字符串类型的情况下;又比如,在字典的键类型为宽度为 4 或 8 的整数类型的情况下。

2.可以在值为 nil 的字典上读写吗?

为了避免烧脑太久,我们再来说一个简单些的问题。由于字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是 nil。在这样一个变量上试图通过键值获取对应的元素值,或者添加键 - 元素对,会成功吗?这个问题虽然简单,但却是我们必须铭记于心的,因为这涉及程序运行时的稳定性

下面来说一下答案。除了添加键 - 元素对,我们在一个值为nil的字典上做任何操作都不会引起错误。当我们试图在一个值为nil的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个 panic。

package main

import "fmt"

func main() {
	var m map[string]int

	key := "two"
	elem, ok := m["two"]
	fmt.Printf("The element paired with key %q in nil map: %d (%v)\n",
		key, elem, ok)

	fmt.Printf("The length of nil map: %d\n",
		len(m))

	fmt.Printf("Delete the key-element pair by key %q...\n",
		key)
	delete(m, key)

	elem = 2
	fmt.Println("Add a key-element pair to a nil map...")
	m["two"] = elem // 这里会引发panic。
}

只有最后一步添加元素时会引发 panic: 

四.总结

通过本文我们可以了解到 map 是如何通过 k 去映射 v,以及存储时 k 的限制,最后注意那些可能引起 panic 的操作,例如在一个 nil 的字典里添加 kv 对,这里感觉也比较有意思,如果是 java 就直接 null point 了,这里却还能操作其他方法,奇怪的知识增加了。

  • 47
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BIT_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值