Go语言核心36讲 09字段的操作与约束

关于字段map的底层结构

字典(map)存储的不是单一值的集合,而是键值对的集合。(键 - 元素对)

Go 语言的字典类型其实是一个哈希表(hash table)的特定实现。

在这个实现中,键和元素的最大不同在于,前者的类型是受限的,而后者却可以是任意类型的。

1. 哈希表中的:映射

先了解哈希表中最重要的一个过程:映射。 你可以把键理解为元素的一个索引,我们可以在哈希表中通过键查找与它成对的那个元素。

键和元素的这种对应关系在数学里就被称为“映射”,这也是“map”这个词的本意,哈希表的映射过程就存在于对键 - 元素对的增、删、改、查的操作之中。

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

1.1 映射规则过程:

第一步:把键值转换为哈希值。

我们要在哈希表中查找与某个键值对应的那个元素值,那么我们需要先把键值作为参数传给这个哈希表。哈希表会先用哈希函数(hash function)把键值转换为哈希值。

第二步:用哈希值低几位去定位到哈希桶(bucket)

哈希值通常是一个无符号的整数。

一个哈希表会持有一定数量的桶(bucket),也可称之为哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的那些键 - 元素对。

因此,哈希表会先用这个键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。

由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。

在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈希值。

第三步:在定位到的哈希桶中查找键值

哈希桶的结构:

哈希桶里的结构是,“键的哈希值-内部结构”对的集合,这个内部结构的结构是“键1元素1键2元素2键3元素3”,是一块连续的内存。在通过键的哈希值定位找到哈希桶和那个“键的哈希值-内部结构”对之后,就开始在这个内部结构里找有没有这个键。

在讲第三步时,我们引入一个小知识点:

字典的键类型的值之间必须可以施加操作符 == 和 != (即为 键类型的值必须要支持判等操作)

由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键类型不能是这些类型。

对于键类型为 interface{} 接口类型的键值类型也不能是上面三种,数组同理。

你可能会有疑问,为什么键类型的值必须支持判等操作?

Go 语言一旦定位到了某一个哈希桶,那么就会试图在这个桶中查找键值:

1.2 为什么键类型的值必须支持判等操作?(哈希桶如何查找键值)

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

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

b:如果有相等的,那就再用键值本身去对比一次。为什么还要对比?原因是,不同值的哈希值是可能相同的。这有个术语,叫做“哈希碰撞”。

所以,即使哈希值一样,键值也不一定一样。

如果键类型的值之间无法判断相等,那么此时这个映射的过程就没办法继续下去了。最后,只有键的哈希值和键值都相等,才能说明查找到了匹配的键 - 元素对。

1.3 小思考:在值为 nil 的字典上执读操作会成功吗?那写操作呢?

字段是引用类型,所以当我们仅声明而不初始化一个字典类型的变量时,它的值就是nil

正确答案:除了添加键 - 元素对,我们在一个值为 nil 的字典上做任何操作都不会引起错误。

当我们试图在一个值为 nil 的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个 panic。

2. 字典类型的值是并发安全问题

关于这个问题,可以看我的另外一篇博客

goroutin 与 map 并发的采坑事件