json.Unmarshal 奇怪的坑

nove001發表於2019-05-16

encoding/json 是 Go 程式碼經常使用的包,但是,可能很多人都會忽略下面這段說明:

To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

當 json 解碼到 interface 型別的變數值時,會將 JSON numbers(實質是 string 型別,表示整數或浮點數數字字串)都當作型別 float64 儲存。

試想以下程式碼輸出?

package main

import (
    "fmt"
    "encoding/json"
    "reflect"
)

func main() {
    s := `{"name":"test","it":1021,"timestamp":1557822591000,"mmp":{"a":"ax","b":999999}}`
    type st struct{
        Name string
        Timestamp interface{}
        Mmp interface{}
        It interface{}
    }
    var tmp st

    err := json.Unmarshal([]byte(s),&tmp)
    fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)

    fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)
}

tmp: {Name:test Timestamp:1.557822591e+12 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
float64, 1.557822591e+12

完成 Json 解碼後,Timestamp 型別為 float64。這顯然是無法讓人接受的,就這裡來說,時間戳應該是 int64 才對。目前,有兩個解決辦法:

  1. 顯示宣告型別

避免使用 interface,而是直接靜態型別指定,在大多數情況下,Json 字串結構都是已知的,靜態的。 上面的場景,就可以將時間戳屬性定義為 Timestamp int64

  1. 使用函式 UseNumber()

func (*Decoder) UseNumber() 使解碼器將數字作為 json.Number 型別, 而不 float64 解碼到 interface 變數。

    ...

    ds := json.NewDecoder(strings.NewReader(s))
    ds.UseNumber()
    err := ds.Decode(&tmp)

    fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)

    //rf, _ := strconv.ParseFloat("123.90",64)
    fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)

tmp: {Name:test Timestamp:1557822591000 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
json.Number, 1557822591000

可以看到,json.Number 其實就是字串型別:

type Number string

因此,這裡其實就是保留原始字串,延遲解析。在需要的時候,使用提供的函式 Float64(), Int64() 等轉化成對應的型別,其實,這些函式的實現就是使用 strconv 包將字串轉化成整型或浮點型。

但是,這裡引入了一個新的型別 json.Number,會侵入到別的無關的程式碼中,也就是說,可能會導致,在其它模組,不得不在型別判斷時,加入 json.Number case。這種耦合是比較讓人難受的。

遺憾的是,目前看來,只有這兩種方式了,雖然都不夠優雅。

這是個很奇怪的問題,因為技術上來說,將數字字串分別解析為整型或浮點型並不難實現,Go 編譯器就很好的實現了(想想 x:=100x:=100.0 的區別); 而且,如果 Json 數字的含義是整型,預設卻解析成 float64 就會有精度丟失的問題,因為 int64 比 float64 表示的範圍更大。

Go issues 找了下,也並沒有看到合理的解釋,難道只是為了實現方便,偷了個懶?真是個奇怪的坑!

相關 issues:

更多技術文章分享

相關文章