json.Unmarshal 奇怪的坑
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 才對。目前,有兩個解決辦法:
- 顯示宣告型別
避免使用 interface,而是直接靜態型別指定,在大多數情況下,Json 字串結構都是已知的,靜態的。
上面的場景,就可以將時間戳屬性定義為 Timestamp int64
。
- 使用函式 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:=100
與 x:=100.0
的區別);
而且,如果 Json 數字的含義是整型,預設卻解析成 float64 就會有精度丟失的問題,因為 int64 比 float64 表示的範圍更大。
去 Go issues 找了下,也並沒有看到合理的解釋,難道只是為了實現方便,偷了個懶?真是個奇怪的坑!
相關 issues:
- https://github.com/golang/go/issues/27082
- https://github.com/golang/go/issues/5562
- https://github.com/golang/go/issues/14159
更多技術文章分享
相關文章
- [系列] Go - json.Unmarshal 遇到的小坑GoJSON
- NIO的奇怪事件 非常奇怪! 救救我!_!事件
- 奇怪的GCDGC
- 奇怪的“物件”物件
- 同樣網路結構,不一樣的推理速度?--記一次奇怪的踩坑
- WriteFile 奇怪的現象
- 奇怪++操作
- 奇葩鍵盤故事多 TrewGrip長的奇怪營銷更奇怪!
- 奇怪的session混亂問題Session
- 奇怪的字串最佳化字串
- 奇怪的DP最佳化
- 奇怪的漢諾塔 - 題解
- 奇怪!!奇怪!真是不可理解。哪位大哥幫幫忙。
- MySQL:一個奇怪的hang案例MySql
- 2個奇怪的React寫法React
- Google 面試題 | 奇怪的印表機Go面試題
- itoa函式的奇怪問題函式
- 遇到mysql的奇怪問題了MySql
- 老大救命,奇怪的異常NoSuchFieldErrorError
- 關於session的奇怪問題Session
- MySQL複製的奇怪問題MySql
- jsp中的奇怪問題JS
- 奇怪的圖論最佳化圖論
- 一個奇怪的Golden Gate的問題Go
- 分析go中slice的奇怪現象Go
- 阿里有三大奇怪的員工阿里
- 奇怪的知識點增加了
- Python——奇怪的掃碼登入Python
- Failed to execute aapt的奇怪解決方法AIAPT
- 20 種最奇怪的程式語言
- 使用strace分析exp的奇怪問題
- [求助] start with connect by 奇怪的問題
- Android開發之奇怪的FragmentAndroidFragment
- 一個奇怪的Java集合問題Java
- 一次奇怪的的bug排查過程
- JavaScript 中的一些奇怪問題JavaScript
- 洛谷P1852 奇怪的字串字串
- 奇怪的登入問題及解決