在 Go 中安全地 json.Marshal 序列化 int64 等数值类型
Javascript 的基础类型(POD)和 JSON 里面其实就只有一种数值类型:Number。 Number 在主流浏览器实现上通常采用 IEEE-754 中的 64-bit 浮点数标准表示(即双精度浮点数),其表示的有效数字的数值的范围在 ~ 之间。 而在 Go 语言中经常使用 64-bit 数据类型,比如:int64/uint64,这样的数值是不能安全地在 Javascript 中使用的。
如果查看 JSON 的规范文档的话,数值是没有限制位数的,任意大的数值都可以放在 JSON 中。
Go 语言中 json 大数的行为
实际用 Go 语言的 json
包来测试,它也确实会直接输出非常大的数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
输出结果:
9223372036854775807 <nil>
{"N":85070591730234615847396907784232501249} <nil>
这几个数明显大于了 ,Go 的 json 包依然把它们直接编码在了 JSON 中,这是符合规范的。
Javascript 中的安全的数
上面的数值,在 Javascript 中,它们并不是安全的数:
1 2 3 4 5 6 7 8 |
|
可以看到,前两句输出为 false
;第三句 parse 出来的结果直接就不等于原本的数了。
一个数在 Javascript 中不安全即意味着数据错误、精度丢失、运算错误。
再比如下面这个 MDN 官方的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
在 Go 中使用字符串序列化大数
虽然 JSON 本身支持任意大小的数,但是 JSON 的实现却未必支持。比如浏览器中的 JSON 对象就不支持。 所以,为了跨语言、跨平台、安全地使用 JSON 作为数据交换格式,像是 Go 语言中的 int64 这种类型的数据,应该使用字符串来编码。
很显然,Go 语言肯定早就考虑到了这一点:我们只需要要在结构体字段的 JSON TAG 中加上一个 string
即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
输出结果:
1 2 |
|
可以看到,数值被编码成了字符串,并且在反序列化的时候也正确。
有一点比较遗憾的是,这个 string
TAG 只针对以下几种类型:字符串、浮点数、整数、布尔。
所以像是以下类型中的N
是不会被字符串化的,需要自己做(本例未做):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
输出:
1 2 |
|
反序列化到 interface{}
另外还有一个反序列化的问题值得一提:当使用 interface{}
作为要反序列化的目标存储时,数值类型是 float64。
这就像是浏览器的行为,同样可能会导致数据丢失(float64 不足以容纳 int64):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
输出结果:
1234567890777888999 float64 1.234567890777889e+18 1234567890777889024
很明显地看到有数据丢失了(原数字有 19 位有效数字,其浮点数只有 16 位有效数字),但是却没有报错。
一种可能的办法是让 json
使用其 json.Number
类型来存储数值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
输出结果:
json.Number
1234567890777888999
1.234567890777889e+18 <nil>
1234567890777888999 <nil>
1234567890777888999
如果这个这个数值太大的话,Int64()
是会报错的,Float64()
不会报错(有点怪异的行为):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
输出结果:
json.Number
123456789012345678901234567890
1.2345678901234568e+29 <nil>
9223372036854775807 strconv.ParseInt: parsing "123456789012345678901234567890": value out of range
123456789012345678901234567890
或者,可以简单地判断 Float64() == String()
?
Protocol Buffers 对 int64 的处理
从 golang/protobuf 可以看到,Protocol Buffers 对所有的 int64/uint64 默认序列化为字符串:
1 2 3 |
|
性能问题?
可能有人觉得把数值作为字符串来传输是不是会较大的性能影响?
其实我觉得不然,因为我们在讨论的是 JSON。JSON 中的字符串和数值在什么区别?不过是字符串多两个引号而已。 用字符串表示数值是在内存中占用的内存较多,而在传输层面上来说,仅仅是多两个字节。理论上对性能影响较小。