zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Go 100 mistakes之常见的JSON错误

2023-02-26 09:48:25 时间

本文是对 《100 Go Mistackes:How to Avoid Them》 一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解

本节将介绍3个和JSON相关的常见错误。

1. 空JSON

首先,我们解决一个反复出现的问题:将一个类型编码成空JSON。

我们首先定义一个point结构体:

type point struct {
  x float32
  y float32
}

这个结构体代表一个具有两个字段的笛卡尔点:x和y。然后我们创建一个point实例并使用标准的json.Marshal函数把该实例编码成一个JSON输出:

p := point{3., 2.5}
b, err := json.Marshal(p) ①
if err != nil {
  return err
}
fmt.Println(string(b)) ②

① Marshal p

② b是一个[]byte变量,我们需要把它转换成可读的字符创。

不幸的的,上面的输出是空:

{}

为什么输出会是空呢?是因为我们忘记在结构体中设置JSON标签了吗?在Go中,结构体的标签是出现在字段类型定义后面的标记符。我们可以使用标签强制字段名称:

type point struct {
  x float32 `json:"x"` ①
  y float32 `json:"y"` ②
}

① 设置x的JSON标签

② 设置y的JSON标签

然而,输出仍然为空:{ }。实际上,对于marshal/unmarshal JSON数据的时候设置JSON标签不是必须的。默认情况下,JSON字段的名称会和结构体字段的名称相同。

那是因为该类型没有被导出吗?在Go中,如果字段名称以大写字母开头,那么该字段是被导出的,即公开的。我们将point结构体名称重新命名为 Point:

type Point struct { ①
  x float32
  y float32
}

① 结构体现在是被导出了。

然而,依然是空:{ }。所以,对于marshaled/unmarshaled来说,结构体也不一定要被导出

真正的原因是因为结构体内部的字段没有被导出所导致的。所以,我们尝试使用这个新point结构体:

type point struct {
  X float32 ①
  Y float32 ②
}

① X字段现在被导出了

② Y字段现在被导出了

输出结果不再是空了:

{"x": 3, "Y": 2.5}

因此,要进行marshaled/unmarshaled,结构体的字段必须被导出

在marshaling时,如果我们想忽略一些字段该怎么办呢?

例如,我们可能想要忽略掉password字段。有两种方法。

首先,我们不导出这些字段,即让字段名的首字母小写。

其次,使用JSON标签。即使用 “-” 可以在marshaling/unmarshaling期间将已导出的字段忽略掉:

type Foo struct {
  A string
  b string ①
  C string `json:"-"` ②
}

f := Foo{
  A: "a",
  b: "b",
  C: "c",
}
b, err := json.Marshal(f)
if err != nil {
  return err
}
fmt.Println(string(b))

① 该字段由于是首字母小写,即作用域是私有变量,所以被忽略。

② 该字段因为使用了JSON的标签 "-",所以被忽略了。

因为字段b没有被导出,字段C被强制忽略了,所以将struct经过marshaled后,只包含字段A:

{"A": "a"}

所以,要想在marshaled/unmarshaled中输出,相关的字段必须要被导出。我们不必将整个结构体类型导出或者强制使用JSON标签。我们也可以通过将字段不导出或者使用指定的JSON标签来忽略相关的字段。

2. 结构体中存在匿名字段的处理

在Go语言中,如果我们声明了一个没有名称的字段,这叫做嵌入字段。嵌入字段用于提升嵌入类型的字段和方法,如下:

type Event struct {
  ID int
  time.Time ①
}

① 嵌入的字段

time.Time是一个嵌入字段,因为它没有名称声明。如果我们创建一个Event结构体类型,我们可以在Event结构体层直接访问time.Time的方法。

event := Event{}
second := event.Second() ①

① 如果结构体中没有嵌入time.Time类型,例如我们在上面的结构体中指定的是一个t变量名的字段,我们要访问Second方法时需要使用下面的方法:event.t.Second()

该Second方法被提升为可通过Event结构直接访问的方法。这就是为什么嵌入式字段主要用于结构体或接口中,而不是像int或string之类的基本类型

使用JSON的marshaling方法封装嵌入字段会有什么影响呢?我们将实例化一个Event示例并把他marshal成JSON格式。下面的这段代码将输出什么呢?

event := Event{
  ID: 1234,
  Time: time.Now(), ①
}
b, err := json.Marshal(event)
if err != nil {
  return err
}
fmt.Printf("json: %s\n", string(b))
① 在实例化一个结构体的时候匿名字段的名称就是嵌入结构体的名字

我们期望该代码输出一下信息:

{"ID":1234,"Time":"2021-04-19T21:15:08.381652+02:00"}

相反,它实际输出是这样的:

"2020-12-21T00:08:22.81013+01:00"

为什么输出会是这样的呢?对于ID字段和其对应的1234值发生了什么?当这个字段被导出时,它应该是已经被marshaled了。要理解这个问题,我们必须澄清两件事情。

首先,如果一个嵌入字段类型实现了一个接口,那么包含该嵌入字段的结构体也将实现这个接口。从某种意义上来说,这是一种继承能力。让我们看一下下面的例子,我们定义了一个Foo结构体和一个嵌入Foo字段的Bar结构体:

type Printer interface { ①
   print()
}
type Foo struct{}
func (f Foo) print() { ②
   fmt.Println("foo")
}
type Bar struct {
   Foo ③
}

① 定义了一个Printer接口和一个print()方法

② 让Foo结构体实现了Printer③ 在Bar结构体中嵌入Foo类型

这里,由于Foo实现了Printer接口,同时Foo是Bar结构体的一个嵌入字段,所以Bar也是一个Printer类型。我们可以实例化一个Bar类型并直接调用print方法,而不通过Foo字段。

其次,我们可以通过构造一个实现了json.Marshaler接口的类型来覆盖掉默认的marshaling行为。该接口包含一个唯一的MarshalJSON方法:

type Marshaler interface {
  MarshalJSON() ([]byte, error)
}

下面是我们自定义marshaling的一个例子:

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))
}

① 定义结构体

② 实现MarshalJSON方法

③ 返回一个静态响应

④ json.Marshal方法将会依赖于自定义的MarshalJSON实现

因为我们已经覆盖了JSON的marshaling行为,所以这段代码将打印出 foo。

澄清了这两点后,再让我们回到上面用Event结构体输出有问题的例子。我们知道time.Time已经实现了json.Marshaler接口。由于time.Time是Event结构体的嵌入字段,提升了time.Time的方法到Event层级。因此,Event也实现了json.Marshaler方法

因此,当我们传递Event到json.Marshal方法时,它不会使用默认的marshaling行为而是time.Time中提供的。这就是为什么在marshaling一个Event时导致忽略了ID字段的原因。

要解决该问题,主要有两种可能的方法。

首先,我们可以给嵌入字段time.Time命名,即不使用嵌入字段:

type Event struct {
  ID int
  Time time.Time ①
}

① time.Time现在不是嵌入字段了

这样,如果我们marshal该版本的Event结构体时,它将打印出如下信息:

{"ID":1234,"Time":"2020-12-21T00:30:41.413417+01:00"}

其次,如果我们想保留或必须保留该time.Time为嵌入字段,另外一种选择是让Event实现json.Marshaler接口。然而,该解决方法更麻烦,而且需要确保该MarshalJSON方法始终与Event结构保持同步。

我们应该小心使用嵌入字段。虽然嵌入类型提升了其字段和方法有时会很方便,但同时也能导致细微的bug,因为父结构体也隐式的实现了该接口。当使用嵌入字段时,我么应该确保了解可能的副作用。

3. JSON和单调时钟

操作系统会处理两种不同的时钟类型:墙上时钟(wall)和单调时钟(monotonic)。在本节中,我们将会看到当time.Time和JSON一起使用时可能产生的影响,并了解为什么这种时钟差异对于理解至关重要。

在下面的例子中,我们将继续使用Event结构体,但是只包含一个time.Time字段(非嵌入字段):

type Event struct {
  Time time.Time
}

我们将实例化一个Event,并将它marshal成JSON,并将JSON串unmarshal成另外一个结构体。然后,我们将比较这个两个结构体。让我们看看marshaling/unmarshaling过程是否总是可逆的:

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
}
isEquals := event1 == event2
① 获取当前本地时间② 实例化一个Event结构体③ Marshal成JSON④ Unmarshaling JSON成结构体实例

那么isEquals应该是什么值?它会是false,而非true。我们该如何解释呢?

首先,让我们打印event1和event2的内容:

fmt.Println(event1.Time) ①
fmt.Println(event2.Time) ②

① 2021-01-10 17:13:08.852061 +0100 CET m=+0.000338660

② 2021-01-10 17:13:08.852061 +0100 CET

我们看到打印的是两个不同的值。event1的值接近于event2的值,除了m=+0.000338660部分。那这部分是什么意思呢?

我们应该知道操作系统提供了两种时钟类型。首先,墙上时钟用于知道当天的当前时间。该时钟可能会发生变化。例如,如果使用NTP(网络时间协议)同步,时钟可以在时间上向后或向前跳跃。我们不应该使用墙上时钟来测量持续时间,因为我们可能会面临一些奇怪的行为,比如负的持续时间。这就是为什么操作系统提供第二种时钟类型的原因:单调时钟。单调时钟保证时间永远都是向前的,并且不会受时间跳跃的影响。它可能会受到潜在频率调整的影响(例如,如果服务器检测到本地石英的移动速度与NTP服务器不同),但不会受到时间跳跃的影响。

在Go中,并没有在两个不同的API中拆分两个类型的时钟,而是time.Time可能同时包含墙上时钟和单调时间。

当我们使用time.Now()获取本地时间时,会返回time.Time的两个时间:

2021-01-10 17:13:08.852061 +0100 CET m=+0.000338660
------------------------------------ --------------
            Wall time               Monotonic time

相反,当我们解析JSON串时,time.Time字段并不包含单调时间,只有墙上时间。因此,当我们比较两个实例时,因为单调时间不一样而输出结果false。这也是我们在打印两个结构体时注意到差异的原因。

我们该如何解决这个问题呢?有两个方法。

第一,我们可以使用Equal方法作为替代。因为当我们使用 == 操作符对两个time.Time进行比较时,它会比较结构体的所有字段包括,包括单调时间部分。所以,我们使用Equal方法来避免这种情况:

areTimesEqual := event1.Time.Equal(event2.Time) ①

① true

Equal方法不会考虑单调时间。因此,areTimesEqual会是true。然而,在这个例子中,我们只比较了time.Time字段,并没有比较它的父结构Event结构体。

第二,依然保持使用 == 操作符来对两个结构体实例进行比较,但使用Truncate方法将单调时间替换成0值:

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
}
isEquals := event1 == event2

① 去除单调时间

② 使用 == 操作符对两个结构体实例进行比较

在此版本中,这两个time.Time字段是相等的。因此,isEquals将会返回true。

总而言之,marshaling/unmarshaling处理的程序并不总是可逆的,我们会遇到在结构体中包含time.Time字段的场景。例如,我们应该牢记这一原则,以免编写错误的测试。