在Go语言中,我们可以很方便的使用标准库encoding/json进行结构体的(反)序列化,它会自动帮我们解析嵌套的structs,以及内部的string、int、map等基本类型,但对于某些特殊类型就不是那么好用了。

举例

一个典型就是标准库time.Time,在结构体中使用该类型表达时间,对后续编程操作非常方便。

但是如果我们对它进行JSON编码,你会发现序列化后的时间格式并不是我们想要的,代码如下:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
 
import (
"encoding/json"
"fmt"
"time"
)
 
type Demo struct {
T time.Time
}
 
func main()  {
d := Demo{T: time.Now()}
s, _ := json.Marshal(d)
fmt.Println(string(s))
}

输出如下:

Go
1
{"T":"2020-09-09T18:00:49.18734+08:00"}

我们当然希望时间格式编码为这样:

Go
1
2020-09-09 18:11:34

这里就要首先搞明白json.Marshal方法是如何编码time.Time字段的,才能进一步解决自定义格式问题。

time.Time编码原理

json.Marshal会反射传入的Demo对象,查看它里面的每个字段,然后遇到了T time.Time。

对于内置类型(例如int、string等),它会直接编码,但现在是time.Time自定义类型,它会做点额外的判定,说明如下:

1
2
3
4
5
6
7
8
9
// Marshal traverses the value v recursively.
// If an encountered value implements the Marshaler interface
// and is not a nil pointer, Marshal calls its MarshalJSON method
// to produce JSON. If no MarshalJSON method is present but the
// value implements encoding.TextMarshaler instead, Marshal calls
// its MarshalText method and encodes the result as a JSON string.
// The nil pointer exception is not strictly necessary
// but mimics a similar, necessary exception in the behavior of
// UnmarshalJSON.

它会反射判定2个事实,首先是看一下time.Time有没有实现如下接口:

Go
1
2
3
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

如果有的话,那么就会执行time.Time.MarshalJSON()得到自定义序列化后的字符串。

如果没有实现该接口,还会再判定有没有实现如下接口:

Go
1
2
3
type TextMarshaler interface {
    MarshalText() (text []byte, err error)
}

作用一样,也是自定义序列化,返回一个字符串。

那么这俩接口有啥区别呢?不如看看time.Time类型的实现,它把两者都实现了,怎么用就是上层的问题了:

Go
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
// MarshalJSON implements the json.Marshaler interface.
// The time is a quoted string in RFC 3339 format, with sub-second precision added if present.
func (t Time) MarshalJSON() ([]byte, error) {
if y := t.Year(); y < 0 || y >= 10000 {
// RFC 3339 is clear that years are 4 digits exactly.
// See golang.org/issue/4556#c15 for more discussion.
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
}
 
b := make([]byte, 0, len(RFC3339Nano)+2)
b = append(b, '"')
b = t.AppendFormat(b, RFC3339Nano)
b = append(b, '"')
return b, nil
}
 
// MarshalText implements the encoding.TextMarshaler interface.
// The time is formatted in RFC 3339 format, with sub-second precision added if present.
func (t Time) MarshalText() ([]byte, error) {
if y := t.Year(); y < 0 || y >= 10000 {
return nil, errors.New("Time.MarshalText: year outside of range [0,9999]")
}
 
b := make([]byte, 0, len(RFC3339Nano))
return t.AppendFormat(b, RFC3339Nano), nil
}

核心区别在于,MarshalJSON需要返回带引号的字符串:

Go
1
return "\"2020-09-09T18:00:49.18734+08:00\""

可以想想encoding/json库会直接把返回值拼到最终JSON串上。

MarshalText则简单的多,直接返回不带引号的部分即可:

Go
1
return “2020-09-09T18:00:49.18734+08:00”

所以总结一下,如果我们自定义的类型想序列化成int这种数字,只能用MarshalJson来返回一个例如”5″这样的字符串(注意没有嵌套引号)。

改变time.Time序列化实现

我们可以通过给time.Time定义一个类型别名,然后给新类型自定义MarshalText方法,同时还能兼顾到time.Time原生方法的调用,一举两得。

如果我们仅仅定义新类型,那么JSON库反射新类型并不能找到序列化接口的实现,因此结果是空的:

Go
1
2
3
4
5
6
7
8
9
10
type MyTime time.Time
type Demo struct {
T MyTime
}
 
func main()  {
d := Demo{T: MyTime(time.Now())}
s, _ := json.Marshal(d)
fmt.Println(string(s))
}

输出:

Go
1
{"T":{}}

现在,我们实现序列化接口:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type MyTime time.Time
type Demo struct {
T MyTime
}
 
func (myT MyTime) MarshalText() (data []byte, err error) {
t := time.Time(myT)
data = []byte(t.Format("2006-01-02 15:04:05"))
return
}
 
func (myT *MyTime) UnmarshalText(text []byte) (err error) {
t := (*time.Time)(myT)
*t, err = time.Parse("2006-01-02 15:04:05", string(text))
return
}
 
func main()  {
d := Demo{T: MyTime(time.Now())}
s, _ := json.Marshal(d)
fmt.Println(string(s))
}

结果达到预期:

Go
1
{"T":"2020-09-09 18:45:49"}

非常值得注意的是,反序列化UnmarshalText需要实现在*MyTime指针类型上,因为我们要在内部改变MyTime对象的值。

总结

两种序列化interface都有用,需要理解它们的场景区别,基本就是这样。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~