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
- 奇怪的GCDGC
- 奇怪的農民
- 同樣網路結構,不一樣的推理速度?--記一次奇怪的踩坑
- 一個奇怪的 Bug
- WriteFile 奇怪的現象
- [20230905]奇怪的語法.txt
- 奇怪的字串最佳化字串
- 奇怪的DP最佳化
- [20181120]奇怪的insert語句.txt
- Python——奇怪的掃碼登入Python
- [20201106]奇怪的awr報表.txt
- 奇怪的圖論最佳化圖論
- 奇怪的漢諾塔 - 題解
- 2個奇怪的React寫法React
- [20210802]grep奇怪的過濾.txt
- [20211111]奇怪的ashtop輸出.txt
- [20220822]奇怪的ashtop輸出.txt
- [20221020]奇怪的增量備份.txt
- [20210924]awk奇怪的輸出.txt
- MySQL:一個奇怪的hang案例MySql
- [20190306]奇怪的查詢結果.txt
- 分析go中slice的奇怪現象Go
- Failed to execute aapt的奇怪解決方法AIAPT
- 20 種最奇怪的程式語言
- [20231012]奇怪的執行時長.txt
- [20230426]奇怪的AVG_IOW_MS.txt
- 阿里有三大奇怪的員工阿里
- 奇怪的知識點增加了
- 一次奇怪的的bug排查過程
- j2me rms 的奇怪問題
- [20180417]奇怪的grep過濾問題.txt
- [20211210]優化遇到的奇怪問題.txt優化
- [20211018]奇怪的歸檔目的地.txt
- [20221103]奇怪的mail資訊(整理版本).txtAI
- JavaScript 中的一些奇怪問題JavaScript
- [20210924]awk奇怪的輸出2.txt
- JavaScript 奇怪事件簿JavaScript事件