golang json 标准库反序列化注意事项以及可行的解决方案

506 阅读4分钟

这次谈论的是做开发的这些年踩过的坑,之一(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。很明显,我们期望看到的不是这个结果。

但是, 与其抛出问题, 但是站在用户的角度更希望有一个解决方案。 是的, 这种情况下还是有解决方案的, 只拿其中三种来做举例:

  1. 使用 go 标准库 json.Number 类型替换默认的类型
  2. 使用 github.com/bytedance/sonic
  3. 使用 github.com/json-iterator/go
  4. 其他

标准库 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 用以确保我们逻辑的严谨性。