字典(Map):map[K]T
K:为键类型,T:为元素(值)类型。
例:map[int] string 一个键类型为int,值类型为string的字典类型
Go语言的字典类型(map)实际上是一个哈希表(hash table)的特定实现,但是字典类型的数据类型是受限的,而哈希表是可以任意类型。
键和元素的这种对应关系叫做“映射”,键就是一个索引,我们通过在哈希表中查找键从而获得与它对应的那个元素。
哈希表的查找过程:
首先需要先把键名作为参数,传给哈希表,哈希表会用哈希函数(hash function)把键值转换为哈希值。
哈希值是一个无符号的整数,哈希表先用这个键的哈希值的低几位去定位到一个哈希桶(bucket,均匀地存储所属哈希表中的键-元素对),
然后再去这个哈希桶中查找这个键,而一旦找到了这个键,就能找到对应的元素值。
随后,哈希表就会把元素值作为结果返回。
综上所述,映射过程的第一步就是把键值转换为哈希值。在Go语言的字典类型中,每个键值都是由它的哈希值代表的,字典不会存储任何键值,只会存储它们的哈希值。
Map的赋值方式:
1. 定义map,赋值给map1:
Var map1 map[int]string map1 := map[int]string{1:"a", 2:"b", 3:"c"}
对于字典值来说,如果指定键没有对应的值则默认为该类型的空值。所以map1[5]会返回一个""。
但是这样的话我们就不知道map1[5]到底是""还是map1[5]没有这个值。所以go提供了另外一种写法:
e, ok := map1[5]
针对字典的索引表达式可以有两个求职结果,第二个求职结果是bool类型的。它用于表明字典值中是否存在指定的键值对。
2. 用make进行初始化赋值
map2 := make(map[int]string) map2[1] = “abc” fmt.Println(map2)
map的使用过程中需要注意以下几点:
1、map是无序的,每次打印出来的map都不会一样,它不能够通过index获取,而必须通过key获取;
2、map的长度不是固定的。和slice类似,map也是一种引用类型;
3、内置的len函数同样适用于map,返回map拥有的key的数量;
4、map的值可以很方便的修改。
问题:字典的键类型不能是哪些类型?
回答:不可以是函数类型、字典类型和切片类型。
分析:Go语言规范中规定,在键类型的值之间必须可以施加操作符“==”和“!=”,必须要支持判等操作。
由于函数类型、字典类型和切片类型不支持判等操作,所以不能是这些类型。
另外,如果键的类型是接口类型(map[interface{}]),那么键值的实际类型也不能是上述三种类型,否则会引发panic。
package main func main() { // 示例1。 var badMap1 = map[[]int]int{} // 这里会引发编译错误。 _ = badMap1 // 示例2。 var badMap2 = map[interface{}]int{ "1": 1, []int{2}: 2, // 这里会引发panic。 3: 3, } _ = badMap2 // 示例3。 var badMap3 map[[1][]string]int // 这里会引发编译错误。 _ = badMap3 // 示例4。 type BadKey1 struct { slice []string } var badMap4 map[BadKey1]int // 这里会引发编译错误。 _ = badMap4 // 示例5。 var badMap5 map[[1][2][3][]string]int // 这里会引发编译错误。 _ = badMap5 // 示例6。 type BadKey2Field1 struct { slice []string } type BadKey2 struct { field BadKey2Field1 } var badMap6 map[BadKey2]int // 这里会引发编译错误。 _ = badMap6 }
该例中,用字面量声明了一个字典badMap2,并进行了初始化,包含了三个键-元素对,其中第二个键元素对的键值是[]int{2},元素值是2,从语法上来讲,这样是可以的。
这里的badMap2的类型是接口类型interface{},元素值类型为int,这样的声明不会引起什么错误,Go语言编译器也不会报错。
但是,在运行的时候,Go语言的运行时系统(runtime库)就会抛出panic错误,所以最好不要把字典类的键类型设定为任何接口类型。
如果键的类型是数组类型,那么还要确保该类型的元素类型不是函数类型、字典类型或切片类型。
比如,由于类型[1][]string的元素类型是[string],所以它也不能作为字典类型的键类型。
另外,如果键类型是结构体类型(struct),那么还要保证其中字段的类型的合法性。
为什么键类型必须支持判等操作?
因为只有哈希值和键值都相等,Go才能通过键值找到元素值。
扩展:
Q1: 应该优先考虑哪些类型作为字典的键类型?
从性能的角度讲,在“映射”的过程中,“把键值转换为哈希值”和“把要查找的键值和bucket哈希桶中的键值做比对”是两个比较耗时的操作,
因此,求哈希和判等操作的速度越快,对于的类型就越适合作为键类型。
求哈希为例,宽度越小的类型速度越快,如bool,整数类型、浮点数类型、复数类型和指针类型。
对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。
类型的宽度是指它的单个值需要占用的字节数,如bool、int8和uint8类型的值需要占用的字节数都是1,宽度就是1.
高级类型中,数组类型的值求哈希实际上是依次求得它每个元素的哈希值并键合并,所以速度就取决于元素类型以及长度。
结构体类型的值求哈希,实际上是对它的所有字段求哈希并进行合并,所以关键在于它的各个字段的类型以及字段的数量。
对于接口类型,具体的哈希算法由值的实际类型决定。
不建议使用高级类型作为字典的键类型,因为判等速度慢,而且在他们的值中存在变数。
比如,对于一个数组,我们可以任意改变元素值,在变化前后,它代表了两个不同的键值。
把接口类型作为字段的键类型最危险,会导致panic错误,甚至程序崩溃。
Q2: 在值为nil的字典上执行读操作会成功吗?写操作呢?
demo19.go
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) ", key, elem, ok) fmt.Printf("The length of nil map: %d ", len(m)) fmt.Printf("Delete the key-element pair by key %q... ", key) delete(m, key) elem = 2 fmt.Println("Add a key-element pair to a nil map...") //m["two"] = elem // 这里会引发panic。 }
字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil。
除了添加键元素对,我们在一个值为nil的字典上做任何操作,都不会引起错误。
而如果我们要在这个值为nil的字典中添加键元素对时,Go的runtime系统会抛出一个panic。
思考题:字典类型的值是并发安全的吗?如果不是,那么我们只在字典上添加或删除键元素对的情况下,依然不安全吗?
A:go实现的map不是多协程安全的(并发访问可能导致):
fatal error: concurrent map read and map write
对字典操作最好用1.9之后新加入的sync.Map, 如果不用的话需要对goroutine加读写锁。
非原子操作需要加锁,map并发读写需要加锁,判断一个操作是否是原子的可以使用go run race命令做数据的竞争检测。