浅谈Go语言(4) - map解读

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不是线程安全的,因此需要加锁,一般的方法是,定义一个embededstruct,类似于子类

    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的maphashmap,所以读取遍历的顺序是不保证的,如果业务需要保证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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小爱玄策

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

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

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

打赏作者

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

抵扣说明:

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

余额充值