前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#77 JSON handling common mistakes

Go语言中常见100问题-#77 JSON handling common mistakes

作者头像
数据小冰
发布2022-08-15 15:28:53
6310
发布2022-08-15 15:28:53
举报
文章被收录于专栏:数据小冰
JSON处理常见问题

Go标准库中的encoding/json包提供了对JSON操作支持,本节将介绍使用encoding/json序列化和反序列数据时常见的三个问题。

因类型内嵌导致的序列化问题

下面程序定义了一个Event结构体,该结构体含有一个int类型的字段和一个内嵌time.Time。由于time.Time是内嵌的,所以可以直接通过Event调用time.Time的方法,例如 event.Second().

代码语言:javascript
复制
type Event struct {
        ID int
        time.Time
}

对有内嵌字段的对象进行JSON序列化操作会产生什么影响呢?下面通过具体的程序进行验证说明,这段代码将序列化event,然后打印序列化后的值,你知道它会打印输出什么内容吗?

代码语言:javascript
复制
event := Event{
        ID:   1234,
        Time: time.Now(),
}

b, err := json.Marshal(event)
if err != nil {
        return err
}

fmt.Println(string(b))

我们可能预期上述代码输出如下内容:

代码语言:javascript
复制
{"ID":1234,"Time":"2022-07-05T17:18:00.365499+08:00"}

实际上,它输出如下的内容:

代码语言:javascript
复制
"2022-07-05T17:18:00.365499+08:00"

如何解释这个输出呢?与我们预期的不一致,ID字段的1234序列化后怎么丢失了?该字段ID是可导出的,理应该被序列化。要搞清原因,有两个知识点需要明白。第一点,如果嵌入字段类型实现了某个接口,则包含嵌入字段的结构也实现了此接口,相当于继承。第二点,类型如果实现了json.Marshaler接口的MarshalJSON方法,则会改变该类型序列化结果。

代码语言:javascript
复制
type Marshaler interface {
        MarshalJSON() ([]byte, error)
}

下面程序定义了一个foo结构体,该结构体实现了MarshalJSON方法,代码如下. 由于我们通过实现Marshaler接口更改了默认的JSON序列化行为,所以程序运行输出的内容为foo.

代码语言:javascript
复制
type foo struct{}

func (foo) MarshalJSON() ([]byte, error) {
        return []byte(`"foo"`), nil
}

func main() {
        b, err := json.Marshal(foo{})
        if err != nil {
                panic(err)
        }
        fmt.Println(string(b))
}

理清了上面的两个知识点,现在回到Event结构体序列化时的问题。由于time.Time实现了json.Marshaler接口,它是Event的一个内嵌字段,所以相当于Event也实现了json.Marshaler接口。当将event传给json.Marshal进行序列化时,不会使用默认的序列化方法,而是使用time.Time提供的MarshalJSON方法。这就是导致序列化后ID字段内容丢失的原因。

代码语言:javascript
复制
type Event struct {
        ID int
        time.Time
}

「NOTE: 如果我们使用json.Unmarshal反序列化Event对象时,也会遇到同样的问题。」

有两种主要的方法可以修复此问题。第一种是不使用类型内嵌,添加一个字段名称,像下面这样添加字段Time. 这样对其进行序列化时,它会打印如下内容,与我们预期的一致。

代码语言:javascript
复制
type Event struct {
        ID   int
        Time time.Time
}
代码语言:javascript
复制
{"ID":1234,"Time":"2022-07-06T17:24:55.879474+08:00"}

如果我们想保留或者必须要保留类型内嵌,第二种处理方法是让Event实现json.Marshaler接口。下面的程序实现了一个自定义MarshalJSON方法用来序列化Event类型的对象。在内部处理过程中,定义了一个类似于Event的匿名结构,去掉了类型内嵌,然后对其进行序列化。这种处理方式显然比较麻烦,并且需要确保MarshalJSON方法中的匿名结构与Event结构始终保持一致。

总结,在类型内嵌时需要小心,虽然通过内嵌可以很方便的使用内嵌类型的方法,但也可能导致细微的错误。因为它可以使含有内嵌的结构体潜在的实现某些接口。总之,在使用嵌入字段时,我们应该清楚地了解可能带来的副作用。

代码语言:javascript
复制
func (e Event) MarshalJSON() ([]byte, error) {
        return json.Marshal(
                struct {
                        ID   int
                        Time time.Time
                }{
                        ID:   e.ID,
                        Time: e.Time,
                },
        )
}
因单调时钟导致的序列化问题

在序列化或者反序列化的结构对象含有time.Time类型的字段时,在进行比较的时候有时会遇到意外的错误。所以深入研究time.Time类型,搞清楚它的原理可以帮助我们减少产生问题错误。

操作系统处理的时钟类型有两种:1.墙上时钟 2.单调时钟。本文将首先深入研究分析这两种时钟类型,然后讨论使用JSON序列化和反序列化time.Time时会产生什么问题。

墙上时钟用于告诉人们一天中的当前时间,该时钟是可变化的。例如,如果使用NTP(网络时间协议)同步时间,时钟会在时间上向前或向后跳跃调整。我们不应该使用墙上时钟来测量持续时间,因为可能会遇到像负持续时间这样奇怪的值,这就是操作系统提供第二种单调时钟的原因。顾名思义,单调时钟保证时间只能向前移动,不会受到时间跳跃的影响。但它可能受到潜在频率调整的影响,例如,如果服务器检测到本地石英的移动速度与NTP服务器不同时,即使在这种情况下,时间也不会产生跳跃。

下面例子中定义了一个Event结构体,该结构体包含一个未嵌入的time.Time字段Time. 然后创建一个Event对象,对其进行序列化操作,然后再将序列化后的内容反序列化到另一个Event对象中。最后比较这两个Event对象,观察它们是否相同。

代码语言:javascript
复制
type Event struct {
        Time time.Time
}

下面的这段代码输出结果是什么?执行后打印的是false而不是true,与我们预期的不一样,如何解释它呢?

代码语言:javascript
复制
t := time.Now()
event1 := Event{
        Time: t,
}

b, err := json.Marshal(event1)
if err != nil {
        return err
}

var event2 Event
err = json.Unmarshal(b, &event2)
if err != nil {
        return err
}

fmt.Println(event1 == event2)

执行下面的代码,将event1和event2内容打印出来,看看到底有何不同。可以看到,确实不一样,event2少了m=+0.000103741这部分内容。

代码语言:javascript
复制
fmt.Println(event1.Time)
fmt.Println(event2.Time)
代码语言:javascript
复制
{2022-07-04 18:00:14.202524 +0800 CST m=+0.000103741}
{2022-07-04 18:00:14.202524 +0800 CST}

在Go语言中,不是有两种API接口分别处理墙上时钟和单调时钟,而是都包含在time.Time结构中。当我们调用time.Now()获取本地时间时,它会返回一个time.Time对象,该对象包含有墙上时钟和单调时钟两种时间信息。

代码语言:javascript
复制
2022-07-04 18:00:14.202524 +0800 CST m=+0.000103741
------------------------------------ --------------
             Wall time               Monotonic time

在进行JSON反序列化时,time.Time字段不包含单调时间,只包含墙上时间。这两个对象是有差异的,所以会输出false. 通过打印对象序列化后字符串也可以验证这一点。

上述问题主要有两种修复方法,第一种是采用Equal进行比较。当我们使用==运算符比较time.Time时,会比较time.Time结构中的所有字段,包括单调时钟部分。为了避免这种情况,可以采用time.Time对象的Equal方法比较,代码如下。

代码语言:javascript
复制
fmt.Println(event1.Time.Equal(event2.Time))

程序的输出结果如下:

代码语言:javascript
复制
true

Equal方法在比较时不会对单调时钟部分进行比较,所以上面的程序会输出true. 但是这种情况,我们只是比较了Event中的Time字段,而不是对整个Event对象进行比较。如果需要比较整个Event,还需编写其他处理代码。

第二种方法是继续使用==运算符来比较两个结构对象,但使用Truncate方法去除单调时间,该方法将time.Time值向下舍入为给定持续时间的倍数。根据官方文档 https://pkg.go.dev/time@master#Time.Truncate 说明,当传入的Duration值d为0时,会去掉单调时钟。

❝func (t Time) Truncate(d Duration) Time Truncate returns the result of rounding t down to a multiple of d (since the zero time). If d <= 0, Truncate returns t stripped of any monotonic clock reading but otherwise unchanged. ❞

代码语言:javascript
复制
t := time.Now()
event1 := Event{
        Time: t.Truncate(0),
}

b, err := json.Marshal(event1)
if err != nil {
        return err
}

var event2 Event
err = json.Unmarshal(b, &event2)
if err != nil {
        return err
}

fmt.Println(event1 == event2)

上面这一版实现通过Truncate方法将t中的单调时钟去除了,所以event1和event2比较输出的结果为true.

「NOTE: 注意time.Time与代表时区的time.Location是相关联的。例如下面时区设置的是CST,因为使用time.Now()返回的是当前本地(北京)的标准时间。对time.Time进行JSON序列化的结果与位置相关,如果不想在序列化时受位置变化干扰,可以通过In方法设置一个特定的位置」

代码语言:javascript
复制
t := time.Now() // 2022-07-04 17:13:08.852061 +0100 CST
代码语言:javascript
复制
// 设置特定的位置
location, err := time.LoadLocation("America/New_York")
if err != nil {
        return err
}
t := time.Now().In(location) // 2021-05-18 22:47:04.155755 -0500 EST

或者使用UTC格式获取当前时间

代码语言:javascript
复制
t := time.Now().UTC() // 2021-05-18 22:47:04.155755 +0000 UTC

总结,序列化和反序列化过程并不是总是对称的,比如本文中Event结构包含了time.Time类型字段就不是完全对称的。对这一点我们应该有所认识,以免编写有问题的程序。

序列化数值到map[T]interface{}存在的问题

在反序列化时,可以将数据反序列化到一个结构体对象中,也可以反序列化到一个map中。当要反序列化的数据中的键和值类型不确定时,反序列化到map中非常方便,因为map能够提供动态性而不是像结构体这样静态的结构。然而,有一个特殊的规则需要我们牢记,否则可能引发panic. 具体是什么通过下面的例子来说明。

下面的程序将数据b反序列化到一个map类型的变量m中,完整代码见https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/77。

代码语言:javascript
复制
b := getMessage()
var m map[string]any
err := json.Unmarshal(b, &m)
if err != nil {
        return err
}

如果返回b的数据是下面的JSON数据。执行上面的程序后,得到m的值为map[id:32 name:foo]. 可以看到数字32和字符串foo都被解析了出来。因为m的value是any类型(interface{}类型别名),支持各种不同类型自动转换。

代码语言:javascript
复制
{
        "id": 32,
        "name": "foo"
}

但有一点需要注意,任何数值,当将它通过JSON反序列化到一个map中时,无论数值是否包含小数,都将被转化为float64类型。下面打印m["id"]类型输出的内容为float64.

代码语言:javascript
复制
fmt.Printf("%T\n", m["id"])

我们应该牢记数值被转换成float64类型这条规则,以确保不会做出错误的假设。例如提供的数值不含小数,转换后我们以为是int类型,实际上是float64, 这时如果对类型做不正确的假设转换可能会产生panic.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JSON处理常见问题
    • 因类型内嵌导致的序列化问题
      • 因单调时钟导致的序列化问题
        • 序列化数值到map[T]interface{}存在的问题
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档