缘起
最近出现一例json.Unmarshal导致的精度丢失引发的线上问题,虽然这个问题在被及时发现,未对业务造成损失,但细挖这个问题的原因仍然比较有意思。这篇文章会从技术层面深入分析json.Unmarshal精度丢失的原因以及处理建议,以避免后续开发过程中再次踩坑。
在分析这个问题的过程中,发现涉及Go对浮点数数值的处理,又涉及IEEE-745标准中的一些细节,放在一篇文章中会增大文章的阅读难度,故拆分成了两个部分:
- Part1: 引出json.Unmarshal处理大整数可能出现精度丢失的问题,并浅层次分析原因以及解决办法。
- Part2: 先补充IEEE-745的背景知识,然后解释为什么json.Unmarshal处理大整数可能会出现精度丢失。
示例
这个问题的现象是,原始json string是一个字典,其中包含了一个大整数,在业务场景中,需要向该字典中追加一些字段,然后再序列化后进行存储。为了使用上的方便,代码中使用map[string]any去接收json.Unmarshal的结果,然后再使用json.Marshal序列化,结果发现序列化后的大整数跟原始大整数不致。
下面代码片段做了一些简化,同时忽略错误处理细节:
1 | str := `{"id":16505201442738640729}` |
上面代码片段输出:
1 | {"id":16505201442738640000} |
原始json string中,id的值是16505201442738640729,经过json.Unmarshal和json.Marshal后,id的值变成了16505201442738640000,看起来出现了精度有丢失。
分析
在上面代码片断中,如果使用any类型接受整型时,json.Unmarshal会默认使用float64存储整型:
1 | str := `{"id":16505201442738640729}` |
输出结果为:
1 | 1.650520144273864e+19 float64 |
在encoding/json包,decode.go中,可以看到其实现:
1 | // convertNumber converts the number literal s to a float64 or a Number |
Go在处理json.Unmarshal时,如果未开启UseNumber,默认会将数值类型的字面量转换成float64。如果启用UseNumber,则会使用json.Number存储,从源码中,我们可以看到,json.Number其实底层就是string:
1 | // A Number represents a JSON number literal. |
看起来问题比较好解决了,可以直接使用json.Decoder,开启UseNumber,json.Unmarshal将数值字面量转化成json.Number,就不会发生精度丢失了。
1 | str := `{"id":16505201442738640729}` |
输出:
1 | 16505201442738640729 json.Number |
至此,看起来问题已经解决了,但是我们仍然没有搞明白字面量转成浮点数时,为什么会发生精度丢失?要回答这个问题涉及Go语言中对浮点数的处理方式。这部分内容留在 Part2 分析。
需要说明的是:在Go语言中,使用json.Unmarshal函数将JSON数据反序列化为Go结构时,整数类型默认被转换为float64类型的原因是JSON中的数字默认为浮点数。 根据JSON规范,数字可以表示为整数或浮点数,而Go语言中的float64类型可以容纳JSON中的所有数字范围,但float64并不能精确表示所有数值。
总结
- json.Unmarshal时,使用any接收时,默认会使用float64存储整数,这可能会导致大整数精度丢失。
- 使用json.Decoder,并开启UseNumber,使用json.Number存储大整数。