前言
一个人不会两次掉进同一个坑里,但是如果他(她)忘记了坑的位置,那就不一定了。
这篇文章记录了最近使用Golang处理JSON遇到的一些坑。
坑
1号坑:omitempty
的行为
C#中最常用的JSON序列化类库Newtonsoft.Json
中,把一个类的实例序列化成JSON,如果我们不想让某个属性输出到JSON中,可以通过property annotation
或者ShouldSerialize method
等方法,告知序列化程序。如下:
1 | // 通过ShouldSerialize method指示不要序列化ObsoleteSetting属性 |
关于Newtonsoft.Json
的Conditional Property Serialization的更多内容参考:
开始使用Golang的时候,以为omitempty
的行为和C#中一样用来控制是否序列化字段,结果使用的时候碰了一头钉子。回头阅读encoding/json package的官方文档,找到对omitempty
行为的描述:
Struct values encode as JSON objects. Each exported struct field becomes a member of the object unless
- the field’s tag is “-“, or
- the field is empty and its tag specifies the “omitempty” option.
The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero. The object’s default key string is the struct field name but can be specified in the struct field’s tag value. The “json” key in the struct field’s tag value is the key name, followed by an optional comma and options. Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 > // Field is ignored by this package.
> Field int `json:"-"`
>
> // Field appears in JSON as key "myName".
> Field int `json:"myName"`
>
> // Field appears in JSON as key "myName" and
> // the field is omitted from the object if its value is empty,
> // as defined above.
> Field int `json:"myName,omitempty"`
>
> // Field appears in JSON as key "Field" (the default), but
> // the field is skipped if empty.
> // Note the leading comma.
> Field int `json:",omitempty"`
>
Golang中,如果指定一个field
序列化成JSON的变量名字为-
,则序列化的时候自动忽略这个field
。这种用法,才是和上面JsonIgnore
的用法的作用是一样的。
而omitempty
的作用是当一个field
的值是empty
的时候,序列化JSON时候忽略这个field
(Newtonsoft.Json
的类似用法参考这里和例子)。这里需要注意的是关于emtpty
的定义:
The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.
通过下面的例子,来加深对empty values
的了解:
1 | package main |
点击这里执行上面的程序
关于empty value
的定义,这里面隐藏了一些坑。下面通过一个例子来说明。
假设我们有一个社交类App,通过Restful API形式从服务端获取当前登录用户基本信息及粉丝数量。如果服务端对Response中User
对象的定义如下:
1 | type User struct { |
如果正在使用App时一个还没有粉丝的用户,访问Restful API的得到Response如下:
1 | { |
这时候你会发现Response的User对象中没有fansCount
,因为fansCount
是个int
类型且值为0,序列化的时候会被忽略。语义上,User
对象中没有fansCount
应该理解为粉丝数量未知,而不是没有粉丝。
如果我们希望做到能够区分粉丝数未知和没有粉丝两种情况,需要修改User
的定义:
1 | type User struct { |
将FansCount
修改为指针类型,如果为nil
,表示粉丝数未知;如果为整数(包括0),表示粉丝数。
这么修改语义上没有漏洞了,但是代码中要给FansCount
赋值的时候却要多一句废话。必须先将从数据源查询出粉丝数赋给一个变量,然后再将变量的指针传给FansCount
。代码读起来实在是啰嗦:
1 | // FansCount是int类型时候 |
2号坑:JSON反序列化成interface{}对Number的处理
JSON的规范中,对于数字类型,并不区分是整型还是浮点型。
对于如下JSON文本:
1 | { |
如果反序列化的时候指定明确的结构体和变量类型
1 | package main |
点击这里执行上面的程序
如果反序列化不指定结构体类型或者变量类型,则JSON中的数字类型,默认被反序列化成float64
类型:
1 | package main |
点击这里执行上面的程序
1 | package main |
点击这里执行上面的程序
从上面的程序可以发现,如果fansCount
精度比较高,反序列化成float64
类型的数值时存在丢失精度的问题。
如何解决这个问题,先看下面程序:
1 | package main |
点击这里执行上面的程序
上面的程序,使用了func (*Decoder) UseNumber
方法告诉反序列化JSON的数字类型的时候,不要直接转换成float64
,而是转换成json.Number
类型。json.Number
内部实现机制是什么,我们来看看源码:
1 | // A Number represents a JSON number literal. |
json.Number
本质是字符串,反序列化的时候将JSON的数值先转成json.Number
,其实是一种延迟处理的手段,待后续逻辑需要时候,再把json.Number
转成float64
或者int64
。
对比其它语言,Golang对JSON反序列化处理真是易用性太差(“蛋疼”)。
JavaScript中所有的数值都是双精度浮点数(参考这里),反序列化JSON的时候不用考虑数值类型匹配问题。这里多说两句,JSON的全名JavaScript Object Notation(从名字上就能看出和JavaScript的关系非常紧密),发明人是Douglas Crockford,如果你自称熟悉JavaScript而不知道Douglas Crockford是谁,就像是自称是苹果粉丝却不知道乔布斯是谁。
C#语言的第三方JSON处理library Json.NET反序列化JSON对数值的处理也比Golang要优雅的多:
1 | using System; |
点击这里执行上面的程序
Json.NET
在反序列化的时候自动识别数值是浮点型还是整型,这一点对开发者非常友好。
3号坑:选择什么格式表示日期
JSON的规范中并没有日期类型,不同语言的library对日期序列化的处理也不完全一致:
Go语言:
1 | package main |
JavaScript语言:
1 | ➜ ~ node |
C#语言:
1 | using System; |
Go的encoding/json
package、C#的Json.NET默认把日期类型序列化成ISO 8601标准的格式,JavaScript默认把Date
序列化从1970年1月1日0点0分0秒的毫秒数。但JavaScript的dateObj.toISOString()
能够将日期类型转成ISO格式的字符串,Date.parse(dateString)
方法能够将ISO格式的日期字符串转成日期。
个人认为ISO格式的日期字符串可读性更好,但序列化和反序列化时的性能应该比整数更低。这一点从Go语言中time.Time
的定义看出来。
1 | type Time struct { |
具体选择哪种形式在JSON中表示日期,有如下几点需要注意:
- 选择标准格式。曾记得.NET Framework官方序列化JSON的方法中,会把日期转成如
"\/Date(1343660352227+0530)\/"
的专有格式,这样的专有格式对跨语言的访问特别不友好。 - 如果你倾向性能,可以使用整数。如果你倾向可读性,可以使用ISO字符串。
- 如果使用整数表示日期,而你的应用又是需要支持跨时区的,注意一定要是从
1970-1-1 00:00:00 UTC
开始计算的毫秒数,而不是当前时区的1970-1-1 00:00:00
。
参考
文章:
- package encoding/json in Go
- http://docs.studygolang.com/src/encoding/json/example_test.go
- The Go Blog: JSON and Go
- Go by example: JSON
- JSON decoding in Go
- go and json
- Decode JSON Documents In Go
- ffjson: faster JSON serialization for Golang
- Serialization in Go
第三方类库:
- ffjson: faster JSON serialization for Go
- go-simplejson: a Go package to interact with arbitrary JSON
- Jason: Easy-to-use JSON Library for Go
- easyjson
- gabs
- jsonparser
工具:
- JSON-to-Go: instantly converts JSON into a Go type definition