Go语言的字典类型其实就是一个哈希表的特定实现。字典的键的类型是受限的,元素的类型可以是任意类型。
字典的键为什么受限呢?
典的
键-元素对
的增删改查的操作,就是哈希表的映射过程。
以查找为例:
- 在哈希表中查找与某个键值对应的元素值的时候,需要先把键值作为参数传给这个哈希表。哈希表用哈希函数把键值转换为哈希值。
哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的哈希桶。
- 用这个键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中查找这个键。
由于键-元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。
- 应的元素值之后,就会将元素值作为结果返回。
我们已经知道了,映射的第一步是把键值转换成哈希值。那么字典的键不能死哪些类型呢?
回答:字典的键不能是函数类型,字典类型,切片类型。
那么,剩下的:基本数据类型,(int系列,float系列,string,复数),数组,结构体,指针,接口等都可以作为键类型。
问题解析
Go语言规范规定:在键类型的值之间必须可以施加操作符==
和!=
。也就是说键类型的值必须要支持判等操作。
注意:
- 键类型是接口类型时,键值的实际类型也不能是上述三种类型。否则会在程序运行时引发panic。
- 最好不要把字典的键类型设定为任何接口类型。
- 如果键类型是数组类型,要确保该类型的元素类型不是函数类型,字典类型或切片类型
- 如果键类型是结构体类型,也要保证其中字段的类型的合法性。不合法的类型被埋藏的多深,都会被Go语言编译器揪出来的。
引申:优先考虑哪些类型作为字典的键类型?
从性能角度
求哈希和判等的速度越快,对应的类型就越适合作为键类型。
宽度越小的类型求哈希的速度通常越快。比如布尔类型,整数类型,浮点数类型,复数类型和指针类型。
对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短,求哈希越快
类型的宽度是指它的单个值需要占用的字节数。比如,bool,int8,uint8类型的一个值需要占用的字节都是1个。因此宽度就都是1。
对于高级类型:
数组:对数组求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度。
结构体:对结构体类型的值求哈希值实际上就是对它的所有字段求哈希值并进行合并。所以关键在于它的各个字段的类型和字段的数量。
接口:对于接口类型,具体的哈希算法由值的实际类型决定。
结论
不建议使用高级数据类型作为字典的键类型。优先选用数值类型和指针类型。通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。
一个是因为对他们求哈希值和判等的速度较慢,还因为他们的值中存在变数。比如,改变数组中任意一个元素,数组的哈希值就变了。
虽然结构体可以通过控制其中字段的访问权限,来防止外界修改它。
把接口类型作为键类型最危险。
在值为nil的字典上执行读操作会成功吗?那写操作呢?
由于字典是引用类型,当我们仅仅声明而不初始化一个字典类型的变量的时候,这个变量的值就是nil。
对这个值为nil的字典进行添加键-元素对,会引发错误。其他操作没有问题。
字典类型的值是并发安全的吗?
非原子操作需要加锁,map操作不是并发安全的,map并发读写需要加锁。
判断一个操作是否是原子的,可以使用go run race
命令做数据的竞争检测。通过 sync.Map 或自己使用sync.RWMutex自己实现并发互斥逻辑