Javascript 的基础类型(POD)和 JSON 里面其实就只有一种数值类型:Number。 Number 在主流浏览器实现上通常采用 IEEE-754 中的 64-bit 浮点数标准表示(即双精度浮点数),其表示的有效数字的数值的范围在 \(-(2^{53} - 1)\) ~ \(2^{53} - 1\) 之间。 而在 Go 语言中经常使用 64-bit 数据类型,比如:int64/uint64,这样的数值是不能安全地在 Javascript 中使用的。
如果查看 JSON 的规范文档的话,数值是没有限制位数的,任意大的数值都可以放在 JSON 中。
Go 语言中 json 大数的行为
json
package main
import (
"encoding/json"
"fmt"
"math"
"math/big"
)
func b2s(b []byte, e error) (string, error) {
return string(b), e
}
type S struct {
N *big.Int
}
func main() {
var i int64 = math.MaxInt64
fmt.Println(b2s(json.Marshal(i)))
bi := (&big.Int{}).Mul(big.NewInt(math.MaxInt64), big.NewInt(math.MaxInt64))
fmt.Println(b2s(json.Marshal(S{N: bi})))
}
输出结果:
9223372036854775807 <nil>
{"N":85070591730234615847396907784232501249} <nil>
这几个数明显大于了 \(2^{53}\),Go 的 json 包依然把它们直接编码在了 JSON 中,这是符合规范的。
Javascript 中的安全的数
上面的数值,在 Javascript 中,它们并不是安全的数:
>> Number.isSafeInteger(9223372036854775807)
<< false
>> Number.isSafeInteger(85070591730234615847396907784232501249)
<< false
>> JSON.parse('9223372036854775807')
<< 9223372036854776000
false
一个数在 Javascript 中不安全即意味着数据错误、精度丢失、运算错误。
再比如下面这个 MDN 官方的示例:
const x = Number.MAX_SAFE_INTEGER + 1;
const y = Number.MAX_SAFE_INTEGER + 2;
console.log(Number.MAX_SAFE_INTEGER);
// expected output: 9007199254740991
console.log(x);
// expected output: 9007199254740992
console.log(y);
// expected output: 9007199254740992
console.log(x === y);
// expected output: true
在 Go 中使用字符串序列化大数
虽然 JSON 本身支持任意大小的数,但是 JSON 的实现却未必支持。比如浏览器中的 JSON 对象就不支持。 所以,为了跨语言、跨平台、安全地使用 JSON 作为数据交换格式,像是 Go 语言中的 int64 这种类型的数据,应该使用字符串来编码。
string
package main
import (
"encoding/json"
"fmt"
"math"
)
type S struct {
A int64 `json:"a,string"`
}
func main() {
s1 := S{A: math.MaxInt64}
b, _ := json.Marshal(s1)
fmt.Println(string(b))
s2 := S{}
json.Unmarshal(b, &s2)
fmt.Println(s2)
}
输出结果:
{"a":"9223372036854775807"}
{9223372036854775807}
可以看到,数值被编码成了字符串,并且在反序列化的时候也正确。
string
N
package main
import (
"encoding/json"
"fmt"
"math"
"math/big"
)
type S struct {
A int64 `json:"a,string"`
N *big.Int `json:"n,string"`
}
func main() {
s1 := S{
A: math.MaxInt64,
N: (&big.Int{}).Mul(big.NewInt(math.MaxInt64), big.NewInt(math.MaxInt64)),
}
b, _ := json.Marshal(s1)
fmt.Println(string(b))
s2 := S{}
json.Unmarshal(b, &s2)
fmt.Println(s2)
}
输出:
{"a":"9223372036854775807","n":85070591730234615847396907784232501249}
{9223372036854775807 85070591730234615847396907784232501249}
interface{}
interface{}
package main
import (
"encoding/json"
"fmt"
"reflect"
)
func main() {
var j = `1234567890777888999`
var i interface{}
if err := json.Unmarshal([]byte(j), &i); err != nil {
panic(err)
}
fmt.Println(j, reflect.TypeOf(i), i, int64(i.(float64)))
}
输出结果:
1234567890777888999 float64 1.234567890777889e+18 1234567890777889024
很明显地看到有数据丢失了(原数字有 19 位有效数字,其浮点数只有 16 位有效数字),但是却没有报错。
jsonjson.Number
package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
func main() {
var j = `1234567890777888999`
var i interface{}
d := json.NewDecoder(strings.NewReader(j))
d.UseNumber()
if err := d.Decode(&i); err != nil {
panic(err)
}
fmt.Println(reflect.TypeOf(i))
n := i.(json.Number)
fmt.Println(j)
fmt.Println(n.Float64())
fmt.Println(n.Int64())
fmt.Println(n.String())
}
输出结果:
json.Number
1234567890777888999
1.234567890777889e+18 <nil>
1234567890777888999 <nil>
1234567890777888999
Int64()Float64()
package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
func main() {
var j = `123456789012345678901234567890`
var i interface{}
d := json.NewDecoder(strings.NewReader(j))
d.UseNumber()
if err := d.Decode(&i); err != nil {
panic(err)
}
fmt.Println(reflect.TypeOf(i))
n := i.(json.Number)
fmt.Println(j)
fmt.Println(n.Float64())
fmt.Println(n.Int64())
fmt.Println(n.String())
}
输出结果:
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 默认序列化为字符串:
case int64, uint64:
w.write(fmt.Sprintf(`"%d"`, v.Interface()))
return nil
性能问题?
可能有人觉得把数值作为字符串来传输是不是会较大的性能影响?
其实我觉得不然,因为我们在讨论的是 JSON。JSON 中的字符串和数值在什么区别?不过是字符串多两个引号而已。 用字符串表示数值是在内存中占用的内存较多,而在传输层面上来说,仅仅是多两个字节。理论上对性能影响较小。