深度解密Go语言之map五(进阶)

深度解密Go语言之map五(进阶)

注:go version go1.9.2 darwin/amd64

1.可以边遍历边删除吗

map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。
一般而言,这可以通过读写锁来解决:sync.RWMutex。
读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。
另外,sync.Map 是线程安全的 map,也可以使用。它的实现原理,这次先不说了。

2.key 可以是 float 型吗

从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是 OK 的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 == 和 != 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。如果是结构体,则需要它们的字段值都相等,才被认为是相同的 key。
顺便说一句,任何类型都可以作为 value,包括 map 类型。

func main() {
	m := make(map[float64]int)
	m[1.4] = 1
	m[2.4] = 2
	m[math.NaN()] = 3
	m[math.NaN()] = 3

	for k, v := range m {
		fmt.Printf("[%v, %d] ", k, v)
	}

	fmt.Printf("\nk: %v, v: %d\n", math.NaN(), m[math.NaN()])
	fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001])
	fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])

	fmt.Println(math.NaN() == math.NaN())
}

程序的输出:

[2.4, 2] [NaN, 3] [NaN, 3] [1.4, 1] 
k: NaN, v: 0
k: 2.400000000001, v: 0
k: 2.4, v: 2
false

例子中定义了一个 key 类型是 float 型的 map,并向其中插入了 4 个 key:1.4, 2.4, NAN,NAN。
打印的时候也打印出了 4 个 key,如果你知道 NAN != NAN,也就不奇怪了。因为他们比较的结果不相等,自然,在 map 看来就是两个不同的 key 了。
接着,我们查询了几个 key,发现 NAN 不存在,2.400000000001 也不存在,而 2.4000000000000000000000001 却存在。
有点诡异,不是吗?
接着,我通过汇编发现了如下的事实:
当用 float64 作为 key 的时候,先要将其转成 unit64 类型,再插入 key 中。
具体是通过 Float64frombits 函数完成:

// Float64frombits returns the floating point number corresponding
// the IEEE 754 binary representation b.
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }

也就是将浮点数表示成 IEEE 754 规定的格式。如赋值语句:

0x00bd 00189 (test18.go:9)      LEAQ    "".statictmp_0(SB), DX
0x00c4 00196 (test18.go:9)      MOVQ    DX, 16(SP)
0x00c9 00201 (test18.go:9)      PCDATA  $0, $2
0x00c9 00201 (test18.go:9)      CALL    runtime.mapassign(SB)

“”.statictmp_0(SB) 变量是这样的:

"".statictmp_0 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40
"".statictmp_1 SRODATA size=8
        0x0000 ff 3b 33 33 33 33 03 40
"".statictmp_2 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40

我们再来输出点东西:

package main

import (
	"fmt"
	"math"
)

func main() {
	m := make(map[float64]int)
	m[2.4] = 2

    fmt.Println(math.Float64bits(2.4))
	fmt.Println(math.Float64bits(2.400000000001))
	fmt.Println(math.Float64bits(2.4000000000000000000000001))
}
4612586738352864255
4612586738352862003
4612586738352862003

转成十六进制为:

0x4003333333333333
0x4003333333333BFF
0x4003333333333333

和前面的 “”.statictmp_0 比较一下,很清晰了吧。2.4 和 2.4000000000000000000000001 经过 math.Float64bits() 函数转换后的结果是一样的。自然,二者在 map 看来,就是同一个 key 了。
再来看一下 NAN(not a number):

// NaN returns an IEEE 754 ``not-a-number'' value.
func NaN() float64 { return Float64frombits(uvnan) }

uvan 的定义为:

uvnan    = 0x7FF8000000000001

NAN() 直接调用 Float64frombits,传入写死的 const 型变量 0x7FF8000000000001,得到 NAN 型值。既然,NAN 是从一个常量解析得来的,为什么插入 map 时,会被认为是不同的 key?
这是由类型的哈希函数决定的,例如,对于 64 位的浮点数,它的哈希函数如下:

func f64hash(p unsafe.Pointer, h uintptr) uintptr {
	f := *(*float64)(p)
	switch {
	case f == 0:
		return c1 * (c0 ^ h) // +0, -0
	case f != f:
		return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaN
	default:
		return memhash(p, h, 8)
	}
}

第二个 case,f != f 就是针对 NAN,这里会再加一个随机数。
这样,所有的谜题都解开了。
由于 NAN 的特性:

NAN != NAN
hash(NAN) != hash(NAN)

因此向 map 中查找的 key 为 NAN 时,什么也查不到;如果向其中增加了 4 次 NAN,遍历会得到 4 个 NAN。
最后说结论:float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用之。

3.总结

在写作本文时,有些问题看遍了中文世界的博客都没能找到解答。当然,源码可以解答任何问题。但是,你不能一下子跳进源码的细节,你得先有一个整体的认识才好。
所以,我开始搜索英文相关讲源码的文章,没有太多这方面的。但是我发现了一篇质量很高的文章,放在了参考资料第一条,它带领读者一步步优化,最终实现了从 map 中随机取出一个 key。推荐你去阅读,非常精彩。尤其是你知道了 map 的底层遍历、扩容的具体过程后更是如此。
总结一下,Go 语言中,通过哈希查找表实现 map,用链表法解决哈希冲突。
通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key。
当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后,原来一个 bucket 中的 key 一分为二,会被重新分配到两个桶中。
扩容过程是渐进的,主要是防止一次扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素,bucket 搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。
查找、赋值、删除的一个很核心的内容是如何定位到 key 所在的位置,需要重点理解。一旦理解,关于 map 的源码就可以看懂了。
最后,如果文章对你有帮助,恳请你帮我分享一下,或者点一下在看,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GoGo在努力

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

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

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

打赏作者

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

抵扣说明:

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

余额充值