用过PHP的人都知道,PHP处理JSON数据那是相当方便,json_encode和json_decode两个函数搞定一切。那么在Go中该怎么处理JSON呢?
学习 json 库应该先了解 Go 中的 struct tag、reflect等知识。
json包实现了json对象的编解码,参见RFC 4627。Json对象和go类型的映射关系请参见Marshal和Unmarshal函数的文档。
参见”JSON and Go”获取本包的一个介绍
对于函数和类型,我们关注经常使用的。
这两个是最常使用的函数,也就是 json 对象的编解码。这两个函数的文档很长,详细解释了 Go 类型和 json 对象的映射关系等。映射关系整理如下:
bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null
详细的编码解码规则,文档上解释的很详细,这里说几个关键点:
①默认情况下,按照上面提到的映射进行解析;
②如果对象实现了 json.Marshaler/Unmarshaler 接口且不是 nil 指针,则调用对应的方法进行编解码;如果没有实现该接口,但实现了 encoding.TextMarshaler/TextUnmarshaler 接口,则调用该接口的相应方法进行编解码;
③struct 中通过 “json” tag 来控制相关编解码,后面通过示例说明;
④struct 的匿名字段,默认展开;可以通过指定 tag 来使其不展开;
⑤如果存在匿名字段,如果同级别有相同字段名,不会冲突,具体处理规则文档有说明;
⑥在解码时到struct时,会忽略多余或不存在的字段(包括不导出的),而不会报错;
另外注意,传递给 Unmarshal 的第二个参数必须是指针。
使用示例:
package mainimport ( "encoding/json" "fmt")func main() { type Book struct { Name string Price float64 // `json:"price,string"` } var person = struct { Name string Age int Book }{ Name: "polaris", Age: 30, Book: Book{ Price: 3.4, Name: "Go语言", }, } buf, _ := json.Marshal(person) fmt.Println(string(buf)) // Output:{"Name":"polaris","Age":30,"Price":3.4} // Book 中的 Name 被忽略了}
如果不希望内嵌类型展开,只需加上 tag:
var person = struct { Name string Age int Book `json:"Book"`}
有时候,比如之前是 PHP(弱类型语言) 写的,Age 的值很可能是 “Age”:”30” 这种形式,现在改为用 Go 实现,为了兼容;或者返回给客户端 Price 这样的浮点值,可能会涉及精度问题,客户端只是单纯的展示,返回浮点值的字符串即可。针对这样的情况,只需要加上这样的 tag:`json:”,string” 即可。这里逗号后面的“string”是 tag option。
如果想忽略某个字段,加上`json:”-”`;如果在值为空时忽略,加上omitempty option,如:`json:”,omitempty”`
在解码时,优先匹配 struct 导出字段的 tag,之后是 Field,最后是 Field 的各种大小写不明感的形式,如 Name,能匹配 NAME/NAme等等。
该函数的功能和 Marshal一致,只是格式化 json,方便人工阅读。如上面例子使用该函数,MarshalIndent(person, “”, “\t”) 输出如下:
{ "Name": "polaris", "Age": 30, "Price": 3.4}
3)Encoder 和 Decoder
有时候,我们可能从 Request 之类的输入流中直接读取 json 进行解析或将编码的 json 直接输出,为了方便,标准库为我们提供了 Decoder 和 Encoder 类型。它们分别通过一个 io.Reader 和 io.Writer 实例化,并从中读取数据或写数据。
通过阅读源码可以发现,Encoder.Encode/Decoder.Decode 和 Marshal/Unmarshal 实现大体是一样;有一些不同点:Decoder 有一个方法 UseNumber,它的作用:默认情况下,json 的 number 会映射为 Go 中的 float64,有时候,这会有些问题,比如:
b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)var person = make(map[string]interface{})err := json.Unmarshal(b, &person)if err != nil { log.Fatalln("json unmarshal error:", err)}age := person["Age"]log.Println(age.(int))
我们希望 age 是 int,结果 panic 了:interface conversion: interface is float64, not int.
我们改为 Decoder.Decode(用上 UseNumber) 试试:
b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)var person = make(map[string]interface{})decoder := json.NewDecoder(bytes.NewReader(b))decoder.UseNumber()err := decoder.Decode(&person)if err != nil { log.Fatalln("json unmarshal error:", err)}age := person["Age"]log.Println(age.(json.Number).Int64())
我们使用了 json.Number 类型。
该类型的定义是 type RawMessage []byte,可见保存的是原始的 json 对象,它实现了 Marshaler 和 Unmarshaler 接口,能够延迟对 json 进行解码。使用示例可以参考 http://docs.studygolang.com/pkg/encoding/json/#RawMessage上的例子。
二、实际应用中的问题
当客户端和服务器通讯使用 json 这种数据格式时,我们一方面会解码客户端的 json 数据,另一方面,需要对数据进行 json 编码,发送给客户端。
一般的,服务器发送给客户端的 json 数据,是通过 struct、[]struct 或 map[string]interface{} 等编码得到。这里除了上文说到的,可能需要对数值类型使用 string tag options 之外,对于 time.Time 类型(实现了Marshaler 接口),默认编码得到的时间格式是:RFC3339即2006-01-02T15:04:05Z07:00,很多时候客户端可能不希望得到这样的时间,他们更多时候只是需要一个可读的时间字符串,如 2006-01-02 15:04:05。对此,我们可以定义自己的类型 type OftenTime time:
func (self OftenTime) MarshalJSON() ([]byte, error) { t := time.Time(self) if y := t.Year(); y < 0 || y >= 10000 { return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") } return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil}func (this *OftenTime) UnmarshalJSON(data []byte) (err error) { t := time.Time(*this) return t.UnmarshalJSON(data)}
另外,有一个坑,json 对象的 key 必须是字符串,所以 map[int]interface{} 在编码时会报错,错误是 json.UnsupportedTypeError.
对于接收客户端数据,进行 json 解码,遇到的问题可能比较多,特别是同时接收多种语言的数据,比如 PHP、Java 等。比如 b := []byte(`{“Name”:”polaris”,”Age”:30,”Money”:20.3}`),PHP 传递过来的可能是:b := []byte(`{“Name”:”polaris”,”Age”:”30″,”Money”:”20.3″}`),在使用 struct 接收数据时,对于 Age,如果是 int,我们可以直接定义为 int 类型,但如果是string,可以通过 string tag options 接收;但如果Age有时是 int, 有时是 string,就会出问题。最理想的情况,当然是不希望出现这种情况,但有一点,程序要保证出现这种情况时,不能 panic。
在实际应用中,我就遇到了上面的问题,于是,自己写了一个 json 解析,能支持自动类型转换。代码开源在 github: https://github.com/polaris1119/jsonutils
很明显,json 的编解码,使用了 Go 的反射功能,所以,性能自然不是太好,正因为如此,有了 ffjson、easyjson 之类的开源库(在 github 上),它们的原理是通过 go generate 根据 struct 生成相应的代码,避免反射。如果你对性能要求比较高,但又不想使用msgpack/pb/thrift 之类的,那么可以考虑使用 ffjson/easyjson 来优化性能。