Go踩坑笔记(十九)

下面代码输出什么?

func main() {
    m := make(map[float64]int)
    m[1.6] = 1 // 10行
    m[math.NaN()] = 2 // 11行

    fmt.Println(m[1.6])
    fmt.Println(m[1.60000001])
    fmt.Println(m[1.60000000000000001])
    fmt.Println(m[math.NaN()])
}

输出:1 0 1 0

这里面有个两个坑点:

第一个 为什么 m[1.60000000000000001] 可以取到值

第二个 为什么 m[math.NaN()] 取不到值。我们一个一个来分析

首先第一个

为什么 m[1.60000000000000001] 可以获取到值,
老样子,犹豫不决,汇编力学~

go tool compile -S main.go | grep main.go:10

输出结果(首尾我都去掉了,只展示比较关键的地方)

// ....
LEAQ    ""..stmp_0(SB), DX
PCDATA  $0, $0
MOVQ    DX, 16(SP)
CALL    runtime.mapassign(SB)
// ....

可以看到当map的key是float64类型的时候,会先将float64类型转成 uin64

具体是通过 math.Float64bits() 函数完成的

// Float64bits returns the IEEE 754 binary representation of f,
// with the sign bit of f and the result in the same bit position,
// and Float64bits(Float64frombits(x)) == x.
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

我们测试一下

fmt.Println(math.Float64bits(1.6))
fmt.Println(math.Float64bits(1.60000000000000001))

输出

4609884578576439706
4609884578576439706

可以看到,1.61.60000000000000001 最后转成 uint64 类型结果是一样的,所以我们使用 m[1.60000000000000001] 也就理所当然的可以获取到值了。

第二个

m[math.NaN()] 为什么取不到值,我们先看下 math.NaN() 函数具体内容

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

其中 uvnan 是一个常量

uvnan = 0x7FF8000000000001

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

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

第二个case: f != f 就是针对 NAN 的,这里会再加一个随机数。
到这里就清楚了,因为 map 的 key 最后其实存的是值的hash,而math.NAN() 返回值的 hash 值都是不同的,所以 m[math.NaN()] 是取不到值的 。

关注我公众号一起学习GO语言

跟派大仙学编程

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!