这次谈论的是做开发的这些年踩过的坑,之一(golang json 标准库序列化)
也许你会好奇地问, json 库不是经常用吗? 这有什么问题呢?这能有什么问题呢? 是的, 一般情况下是没有什么问题的。 但是我们不妨先看一下这一段代码?
package main
import (
"encoding/json"
"fmt"
"reflect"
)
var data = []byte(`{"num":1}`)
func main() {
var obj = make(map[string]interface{})
if err := json.Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(obj)
for k, v := range obj {
fmt.Println("key= ", k, ", value = ", v, ", value type = ", reflect.TypeOf(v).Kind().String())
}
}
output 为
map[num:1]
key= num , value = 1 , value type = float64
这个看起来没有什么问题,map 输出的结果也完全没有什么问题, 但是我们仔细地看观察一下 value type 的输出值, float64, 对的, float64, 但是为什么是 float64 这种类型呢?我们期望的不是整型吗?
float64 是 golang 双精度浮点数的表示,其代表的准确的值是由 1 位的符号位,11 位的指数位和 52 位的小数位来表示一个完整的值 较详细内容参考。
当前这种情况好像是 float64 类型也没有什么问题诶,起码看起来结果是对的。但是, 咱们不妨看一下这种情况。
package main
import (
"encoding/json"
"fmt"
"math"
"reflect"
)
// run in darwin arm64
func main() {
for i := int64(9007199254740990); i < math.MaxInt64; i += 1 {
var obj, dst = make(map[string]interface{}), make(map[string]interface{})
obj["int64"] = i
var data, err = json.Marshal(obj)
if err != nil {
panic(err)
}
if err = json.Unmarshal(data, &dst); err != nil {
panic(err)
}
fmt.Println(
"当前 i 的值 ", i, "\r\n",
"int64(dst[int64]) ", int64(dst["int64"].(float64)), "\r\n",
"dst[int64] ", dst["int64"], "\r\n",
"MaxInt64 - i ", math.MaxInt64-1, "\r\n",
"二进制序列值 ", fmt.Sprintf("%b", i), "\r\n",
"二进制序列值的位数 ", len(fmt.Sprintf("%b", i)), "\r\n",
"dst[int64] 类型 ", reflect.TypeOf(dst["int64"]).Kind().String(), "\r\n",
"==========================================================================================================",
)
if int64(dst["int64"].(float64)) != i {
panic(fmt.Sprintf("data i = %d, dst %v", i, dst))
}
}
}
运行一下看输出的结果
当前 i 的值 9007199254740990
int64(dst[int64]) 9007199254740990
dst[int64] 9.00719925474099e+15
MaxInt64 - i 9223372036854775806
二进制序列值 11111111111111111111111111111111111111111111111111110
二进制序列值的位数 53
dst[int64] 类型 float64
==========================================================================================================
当前 i 的值 9007199254740991
int64(dst[int64]) 9007199254740991
dst[int64] 9.007199254740991e+15
MaxInt64 - i 9223372036854775806
二进制序列值 11111111111111111111111111111111111111111111111111111
二进制序列值的位数 53
dst[int64] 类型 float64
==========================================================================================================
当前 i 的值 9007199254740992
int64(dst[int64]) 9007199254740992
dst[int64] 9.007199254740992e+15
MaxInt64 - i 9223372036854775806
二进制序列值 100000000000000000000000000000000000000000000000000000
二进制序列值的位数 54
dst[int64] 类型 float64
==========================================================================================================
当前 i 的值 9007199254740993
int64(dst[int64]) 9007199254740992
dst[int64] 9.007199254740992e+15
MaxInt64 - i 9223372036854775806
二进制序列值 100000000000000000000000000000000000000000000000000001
二进制序列值的位数 54
dst[int64] 类型 float64
==========================================================================================================
panic: data i = 9007199254740993, dst map[int64:9.007199254740992e+15]
额, 毫无疑问,我们的程序因报错而退出了。 但是我们不妨先分析一下程序为什么报错了。 经过了漫长的分析定位, 我们定位到以下这一段代码会抛出异常。
if int64(dst["int64"].(float64)) != i {
panic(fmt.Sprintf("data i = %d, dst %v", i, dst))
}
我们再来分析一下, 当 i 的值是 9007199254740993, int64(dst[int64].(float64)) 的值是 9007199254740992 结果相差 1。很明显,我们期望看到的不是这个结果。
但是, 与其抛出问题, 但是站在用户的角度更希望有一个解决方案。 是的, 这种情况下还是有解决方案的, 只拿其中三种来做举例:
- 使用 go 标准库
json.Number类型替换默认的类型 - 使用
github.com/bytedance/sonic - 使用
github.com/json-iterator/go - 其他
标准库 json.Number 解决方案
对于任何类型来说, 只要使用字符串来表示,那么就能表示无限长度,当然了,字符串也一样。json 标准库同样也提供了类似的方式来解决这个问题。
json.Number => 底层使用字符串来表示任意的数值类型
我们可以使用 json.Decoder 类型来配置是否使用 json.Number
func Marshal(v any) (data []byte, err error) {
return json.Marshal(v)
}
func Unmarshal(data []byte, v interface{}) (err error) {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
return decoder.Decode(v)
}
具体的 case 如下
package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"reflect"
)
// Marshal 起一个和标准库一样的名字, 这样我们就可以不用大幅度改动源代码直接修 bug 了
func Marshal(v any) (data []byte, err error) {
return json.Marshal(v)
}
func Unmarshal(data []byte, v interface{}) (err error) {
decoder := json.NewDecoder(bytes.NewReader(data))
// 表示使用 json.Number 内部会使用 string type Number string json 解析器会将对应的数值解析成字符串
// 如果使用字符串来表示, 那么我们就可以使用非常大的数字了
decoder.UseNumber()
return decoder.Decode(v)
}
func main() {
var data = []byte(fmt.Sprintf(`{"mark":%d,"float64":%v}`, math.MaxInt64, math.MaxFloat64))
var obj = make(map[string]interface{})
var err error
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(obj)
if data, err = Marshal(obj); err != nil {
panic(err)
}
obj = make(map[string]interface{})
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(
"mark ", obj["mark"], "\r\n",
"obj mark type ", reflect.TypeOf(obj["mark"]).Kind().String(), "\r\n",
// 使用字符串来是否相等来表示经过序列化和反序列化之后来确定是有错误
"MaxInt64 == mark ?", fmt.Sprintf("%v", math.MaxInt64) == fmt.Sprintf("%v", obj["mark"]), "\r\n",
"MaxFloat64 == float64 ?", fmt.Sprintf("%v", math.MaxFloat64) == fmt.Sprintf("%v", obj["float64"]),
)
}
至此, 我们可以使用标准库来解决超大数值反序列化的问题了。
使用 github.com/bytedance/sonic
使用 sonic 库也是一个不错的选择
package main
import (
"encoding/json"
"fmt"
"math"
"reflect"
"github.com/bytedance/sonic"
)
var handler = sonic.Config{
UseNumber: true, // 只能选择这个
//UseInt64: true, // 不可以使用这个选项
}.Froze()
func Marshal(v interface{}) (data []byte, err error) {
return json.Marshal(v)
}
func Unmarshal(data []byte, v interface{}) (err error) {
return handler.Unmarshal(data, v)
}
func main() {
var data = []byte(fmt.Sprintf(`{"mark":%d,"float64":%v}`, math.MaxInt64, math.MaxFloat64))
var obj = make(map[string]interface{})
var err error
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(obj)
if data, err = Marshal(obj); err != nil {
panic(err)
}
obj = make(map[string]interface{})
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(
"mark ", obj["mark"], "\r\n",
"obj mark type ", reflect.TypeOf(obj["mark"]).Kind().String(), "\r\n",
// 使用字符串来是否相等来表示经过序列化和反序列化之后来确定是有错误
"MaxInt64 == mark ?", fmt.Sprintf("%v", math.MaxInt64) == fmt.Sprintf("%v", obj["mark"]), "\r\n",
"MaxFloat64 == float64 ?", fmt.Sprintf("%v", math.MaxFloat64) == fmt.Sprintf("%v", obj["float64"]),
)
}
使用 github.com/json-iterator/go
老牌的 json-iterator 库也是可以解决我们当前遇到的问题的,并且经过封装之后,也可以无感知升级
package main
import (
"encoding/json"
"fmt"
"math"
"reflect"
jsoniter "github.com/json-iterator/go"
)
var handler = jsoniter.Config{UseNumber: true}.Froze()
func Marshal(v interface{}) (data []byte, err error) {
return json.Marshal(v)
}
func Unmarshal(data []byte, v interface{}) (err error) {
return handler.Unmarshal(data, v)
}
func main() {
var data = []byte(fmt.Sprintf(`{"mark":%d,"float64":%v}`, math.MaxInt64, math.MaxFloat64))
var obj = make(map[string]interface{})
var err error
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(obj)
if data, err = Marshal(obj); err != nil {
panic(err)
}
obj = make(map[string]interface{})
if err = Unmarshal(data, &obj); err != nil {
panic(err)
}
fmt.Println(
"mark ", obj["mark"], "\r\n",
"obj mark type ", reflect.TypeOf(obj["mark"]).Kind().String(), "\r\n",
// 使用字符串来是否相等来表示经过序列化和反序列化之后来确定是有错误
"MaxInt64 == mark ?", fmt.Sprintf("%v", math.MaxInt64) == fmt.Sprintf("%v", obj["mark"]), "\r\n",
"MaxFloat64 == float64 ?", fmt.Sprintf("%v", math.MaxFloat64) == fmt.Sprintf("%v", obj["float64"]),
)
}
其他
我们可以使用其他的 json 解析库,如果实力允许的话,自己实现可以的。不过,后者的成本属实是有点大了。
总结
这篇博客主要是总结了使用 json 标准库序列化、反序列化大数值时遇到的精度丢失的问题,以及给出了可行的解决方案。
不过,虽然这些方案可行, 但是如果需要和前端做交互, 那么我们将要面临一个需要解决的问题,就是 js 解析器最大也是允许 9007199254740992 如果超过这这个值, 我们会面临同样的问题, 这个时候, 我们就可能需要考虑把数据类型转换为 string 用以确保我们逻辑的严谨性。