Golang/JSON | 数值的反序列化和空值的序列化

encoding/json包在web开发的过程中,占据着相当重要的角色。然而角色有多重,坑就有多大。下面记录的便是在踩json雷中的辛酸泪(>﹏<)

反序列化时的数值处理及float64精度问题

众所周知(其实是最近才知道),golang原生的encoding/json在反序列化的时候,默认情况下会把所有数值类型转成float64类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"
import "encoding/json"

func test_std_json(){

var m []interface{}
if err := json.Unmarshal([]byte(`[100, null, 1.2, "1234"]`), &m); err == nil {
for _, v := range m {
fmt.Printf("type: %T, value: %v\n",v, v )
}

} else {
fmt.Println("Unmarshal error: ", err)
}
}

调用上面的函数会输出:

1
2
3
4
type: float64, value: 100
type: <nil>, value: <nil>
type: float64, value: 1.2
type: string, value: 1234

一般情况下是不会有什么问题的,但是,如果我们传进去的是一个大整型(超过float64定义的范围),那么就粗大事了。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
func test_std_json_large(){

var m []interface{}
if err := json.Unmarshal([]byte(`[100, 1234567890123456789, null, 1.2, "1234"]`), &m); err == nil {
for _, v := range m {
fmt.Printf("type: %T, value: %v\n",v, v )
}

} else {
fmt.Println("Unmarshal error: ", err)
}
}

调用上面的函数会输出:

1
2
3
4
5
type: float64, value: 100
type: float64, value: 1.2345678901234568e+18
type: <nil>, value: <nil>
type: float64, value: 1.2
type: string, value: 1234

注意到了吗?上面的数字1234567890123456789是一个在int64范围内,但是在float64之外的数值。反序列化之后,这个值变成了123456789012345678!!!试想一下,本来你手头有1234567890个亿,经过json.Unmarshal,就只剩123456789个亿了┭┮﹏┭┮

但是没关系,此事并非不可解。下面我们来看看两种解决方案。

方法一:使用标准库的json.Decoder

golang的标准json库提供了一种方案:将数值类型直接转成json.Number类型,让用户稍后自己根据需要转成数值。具体的实现是利用json库提供的Decoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func test_std_json_large_withNumber(){
var m []interface{}
decoder := json.NewDecoder(strings.NewReader(`[100, 1234567890123456789, null, 1.2, "1234"]`))
decoder.UseNumber()

if err := decoder.Decode(&m); err == nil {
for _, v := range m {
fmt.Printf("type: %T, value: %v\n",v, v )
}

} else {
fmt.Println("Unmarshal error: ", err)
}
}

调用上面的函数会输出:

1
2
3
4
5
type: json.Number, value: 100
type: json.Number, value: 1234567890123456789
type: <nil>, value: <nil>
type: json.Number, value: 1.2
type: string, value: 1234

这样,我们的1234567890个亿还是1234567890个亿。可以把json.Number当成字符串,标准库对于这个类型还提供了一些方便的方法来取出数值。具体可以参考json.Number

以上的代码在:这里

一般来说,这个方法已经很好的解决了float64的精度问题。但是我们还可以找个替代方案。

因为type Number string,所以之前以为使用这种方法,在反序列化之后,无法区分数值和数值字符串。在写这篇文章的时候才突然想起来,在golang中,使用关键字type定义的是新的类型。也就是说,在golang看来,Numberstring是两种不一样的类型。但是如果使用Number,在取值的时候,必不可免需要进行类型判断。所以才会考虑找个替代库。

方法二:换个库吧

jsoniter是国人写的一个用来替代标准库的json库。这个库允许我们自定义类型解析函数。在其github中的某个issue中提到优先把数值当成int64处理的方法。以下代码注册了一个类型解析函数,这个函数会首先尝试把数值解析成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
29
30
import (
"encoding/json"
"github.com/json-iterator/go"
"strconv"
"unsafe"
)

func init() {
decodeNumberAsInt64IfPossible := func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
switch iter.WhatIsNext() {
case jsoniter.NumberValue:
var number json.Number
iter.ReadVal(&number)
i, err := strconv.ParseInt(string(number), 10, 64)
if err == nil {
*(*interface{})(ptr) = i
return
}
f, err := strconv.ParseFloat(string(number), 64)
if err == nil {
*(*interface{})(ptr) = f
return
}
// Not much we can do here.
default:
*(*interface{})(ptr) = iter.Read()
}
}
jsoniter.RegisterTypeDecoderFunc("interface {}", decodeNumberAsInt64IfPossible)
}

之后,我们就可以像使用标准库那样,调用jsoniter的Unmarshal方法进行反序列化了。

当一切成空,序列化时该怎么办

官方文档有云:

1
2
3
a nil slice encodes as the null JSON value.
A nil pointer encodes as the null JSON value.
A nil interface value encodes as the null JSON value.

举个例子:

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
package main

import (
"fmt"
"encoding/json"
)

func main() {
// map
var sm map[string]interface{}
sb, err := json.Marshal(sm)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
// slice
var ss []string
sb, err = json.Marshal(ss)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
var sbs []byte
sb, err = json.Marshal(sbs)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
// pointer
var sp *string
sb, err = json.Marshal(sp)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
// interface
var si interface{}
sb, err = json.Marshal(si)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
}

输出:

1
2
3
4
5
null, [110 117 108 108], err: <nil>
null, [110 117 108 108], err: <nil>
null, [110 117 108 108], err: <nil>
null, [110 117 108 108], err: <nil>
null, [110 117 108 108], err: <nil>

注意哦,这里序列化得到的结果并不是大小为0的byte切片,而是字符串null!!!当然,如果不想输出字符串null,那么可以修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"encoding/json"
)

func main() {
// map
sm := make(map[string]interface{}, 0)
sb, err := json.Marshal(sm)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
// slice
ss := make([]string, 0)
sb, err = json.Marshal(ss)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
// bytes
sbs := make([]byte, 0)
sb, err = json.Marshal(sbs)
fmt.Printf("%s, %v, err: %v\n", string(sb), sb, err)
}

输出:

1
2
3
{}, [123 125], err: <nil>
[], [91 93], err: <nil>
"", [34 34], err: <nil>

此时,输出分别是对应类型的零值的字符串表示。例如,空 Map 的序列化值为字符串{}

所以,如果你只是希望在空的情况下,序列化得出空的结果,那么最好在序列化之前进行一次判空。

参考