文章目录
1. 写在前面
现在基本上所有的编程语言都有自带的map
,或者dict
,主要提供一个快速的查找,插入,删除,具备与存储体量无关的O(1)
的性能,并且支持key上面的唯一性,比如java里的HashMap
,python里的Dictionary
,scala里的各种Map
等等。
Go 语言也原生提供了一个类似的数据类型,叫做map
。首先它是个mutable
的,也就是说,可以随时对其进行修改。其次,它不是线程安全的。等价于java里的HashMap
。
字典(map)存储的不是单一值的集合,而是键值对的集合。
键值对:从英文 key-value pair 直译过来的。
键-元素对:Go 语言规范中,为了避免歧义,统一将键值对换了一种称为:键-元素对。
2. map的哈希实现
Go 语言的字典类型是一个哈希表(hash table)的特定实现,在这个实现中,键和元素的最大不同在于,键的类型是受限的,而元素却可以是任意类型的。
(1) 映射
映射是哈希表中最重要的一个过程。
把键理解为元素的一个索引,可以在哈希表中通过键查找与它成对的那个元素。
键和元素的这种对应关系,在数学里就被称为“映射”,这也是map
这个词的本意。
哈希表的映射过程就存在于对键-元素对的增、删、改、查的操作之中。
国际惯例,上一段map的简单代码:
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的哈希查找
在哈希表中查找与某个键值对应的那个元素值,先把键值作为参数传给这个哈希表。哈希表使用哈希函数将键值转化为哈希值,通常是一个无符号的整数(整型比字符串的比较查找效率高很多),一个哈希表会持有一定数量的桶(bucket),称为哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的键-元素对。哈希表会先用这个键哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。由于键-元素对是被捆绑在一起存储的,所以找到键就找到了这个键对应的元素值并返回这个值。
(3) map的键类型
Go 语言规范中的典型回答是:Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。
Go 语言规范规定,在键类型的值之间必须可以施加操作符==
和!=
。
深入解析1
函数类型、字典类型和切片类型的值并不支持判等操作,所以这是根本原因。
如果是接口类型呢,请看以下代码:
func badMap() {
var badMap = map[interface{}]int{
"1": 1,
[]int{2}: 2, // panic: runtime error: hash of unhashable type []int
3: 3,
}
k := "1"
v, ok := badMap[k]
if ok {
fmt.Printf("The element of key %q: %d\n", k, v)
} else {
fmt.Println("Not found!")
}
}
上述代码,躲过了编译器的检查,运行时还是会包panic
。
所以最好不要把字典的键类型设定为任何接口类型,如果非要这么做,请一定确保代码在可控的范围之内。
深入解析2
为什么键类型的值必须支持判等操作?我们来看这个根本原因的原因。
我们来看Go是如何在哈希桶中查找键值的
- Step1. 每个哈希桶都会把自己包含的所有键的哈希值存起来。
- Step2. 用被查找键的哈希值与这些哈希值逐个对比。
- 没有相等的,说明桶中没有查找的键值
- 有相等的,再用键值本身去对比一次(防止哈希碰撞)
从以上过程能看出,即使哈希值一样,键值也不一定一样。
如果键类型的值之间无法判断相等,那么此时这个映射的过程就没办法继续下去了。
(4) 哈希的原理思考
优先考虑哪些类型作为字典的键类型?
求哈希和判等操作的速度越快,对应的类型就越适合作为键类型。
以求哈希的操作为例,宽度越小的类型速度通常越快。
在值为nil
的字典上执行读/写操作会成功吗?
为什么会这么问呢,由于map
是引用类型,当我们仅声明而不初始化一个map
典类型的变量的时候,它的值会是nil
。
除了添加键-元素对,我们在一个值为nil的map
上做任何操作都不会引起错误。
当我们试图在一个值为nil
的字典中添加键-元素对的时候,Go 语言的运行时系统就会立即抛出一个panic
。
关于线程安全
非原子操作需要加锁, map
并发读写需要加锁,map
操作不是并发安全的,判断一个操作是否是原子的可以使用go run race
命令做数据的竞争检测。
3. map的基本操作
(1) map的创建
两种创建的方式:一是通过字面值;二是通过make函数
func printMapSSValue(myMap map[string]string, key string) {
v, ok := myMap[key]
if ok {
fmt.Printf("The element of key %s: %s\n", key, v)
} else {
fmt.Println("Not found!")
}
}
func creatMap() (map[string]string, map[string]string, map[string]string, map[string]string) {
// 1 字面值
m1 := map[string]string{
"m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
}
// 2 使用make函数
m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
m2["m2"] = "v2" // 添加元素
// 定义一个空的map
m3 := map[string]string{}
m4 := make(map[string]string)
return m1, m2, m3, m4
}
func main() {
m1, m2, m3, m4 := creatMap()
printMapSSValue(m1, "m1")
printMapSSValue(m2, "m2")
_ = m3
_ = m4
}
(2) map的增删改查
直接上代码,注释中有运行结果和基本介绍:
func main() {
m5 := map[string]string{
"a": "va",
"b": "vb",
}
fmt.Println(len(m5)) // len(m) 获得m中key/value对的个数
// 增加,修改
{
// k不存在为增加,k存在为修改
m5["c"] = ""
m5["c"] = "11" // 重复增加(key相同),使用新的值覆盖
fmt.Printf("%#v %#v\n", m5, len(m5)) // map[string]string{"a":"va", "b":"vb", "c":"11"} 3
}
// 查
{
// 查1 - 元素不存在
v1 := m5["x"] //
v2, ok2 := m5["x"]
fmt.Printf("%#v %#v %#v\n", v1, v2, ok2) // "" "" false
// 查2 - 元素存在
v3 := m5["a"]
v4, ok4 := m5["a"]
fmt.Printf("%#v %#v %#v\n", v3, v4, ok4) //"va" "va" true
}
// 删, 使用内置函数删除k/v对
{
delete(m5, "x") // 删除不存在的key,原m不影响
delete(m5, "a") // 删除存在的key
fmt.Printf("%#v %#v\n", m5, len(m5)) // map[string]string{"b":"vb", "c":"11"} 2
delete(m5, "a") // 重复删除不报错,m无影响
fmt.Printf("%#v %#v\n", m5, len(m5)) /// map[string]string{"b":"vb", "c":"11"} 2
}
}
(3) map的遍历
使用for循环遍历k-v:
func main() {
m6 := map[string]string{
"a": "va",
"b": "vb",
"d": "vd",
"e": "ve",
"f": "vf",
}
for key, value := range m6 {
fmt.Println("Key:", key, "Value:", value)
}
}
4. 线程安全(goroutine)
前面提到go的map
不是线程安全的,因此需要加锁,一般的方法是,定义一个embeded
的struct
,类似于子类
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
(1) 读的时候,调用读锁
counter.RLock()
n := counter.m["some_key"] // 当从map中读取一个不存在的key的时候,返回0值
counter.RUnlock()
fmt.Println("some_key:", n)
(2) 写的时候,写锁
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
(3) 读取顺序
go的map
是hashmap
,所以读取遍历的顺序是不保证的,如果业务需要保证key
的遍历顺序,建议将key
单独保存到一个slice
里
func main() {
m := map[int]string{
1: "va",
2: "vb",
3: "vd",
4: "ve",
5: "vf",
}
var keys []int
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}
#Reference:
极客时间 - Go语言核心36讲
https://www.jianshu.com/p/ba7852fbcc80
https://www.cnblogs.com/bingzhen/p/10503967.html