Go踩坑筆記(十九)

pibigstar發表於2020-11-21

下面程式碼輸出什麼?

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 協議》,轉載必須註明作者和本文連結

相關文章