JSON and Go

Golang官方博客 – JSON and Go的翻译,原文地址: https://blog.golang.org/json

知其然知其所以然,写个代码有何难。


正文如下:

简介

JSON(JavaScript Object Notation)是一种简单的数据交换格式。在语法上看,类似于JavaScript的对象和数组。
它最常用于web后端和运行在浏览器中的JavaScript程序之间的通信,但它也用于许多其他地方。
它的官方网站,json.org,提供了一个非常清晰和简洁的标准定义。

通过使用golang的官方json package,用golang来读写JSON数据非常简单。

Encoding

使用Marshall函数来编码JSON数据

1
func Marshal(v interface{}) ([]byte, error)

让我们先定义一个golang数据结构体: Message

1
2
3
4
5
type Message struct {
Name string
Body string
Time int64
}

然后再定义一个Message实例

1
m := Message{"Alice", "Hello", 1294706395881547000}

我们可以使用json.Marshall函数来处理JSON数据:

1
b, err := json.Marshal(m)

如果一切正常,err将会是nil,b将是包含json数据的byte切片:

1
b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

只对那些有效表示JSON的数据结构来编码:

  • JSON对象只支持strings类型作为key;编码Map类型的数据,需要数据满足map[string]T(T是json包下支持的golang类型)
  • 通道,复数和函数类型不支持被编码
  • 不支持循环数据结构;它将导致Marshal函数进入无限循环
  • 指针将被编码为它所指向的值(如果是nil指针,则为null)

在json包输出字段的过程中,只有那些首字母大写的结构体字段会被编码。

Decoding

使用Unmarshal函数来解码JSON数据

1
func Unmarshal(data []byte, v interface{}) error

首先,我们要创建一个存储解码后数据的地方

1
var m Message

之后,调用json.Unmarshal函数,把JSON数据转为[]byte和指向m的指针作为参数传进去

1
err := json.Unmarshal(b, &m)

如果b中包含的有效JSON数据和m相匹配,函数调用后,err为nil,来源于B的数据将会存储在结构体m中,类似于被声明了:

1
2
3
4
5
m = Message{
Name: "Alice",
Body: "Hello",
Time: 1294706395881547000
}

随之而来的问题是Unmarshal函数如何识别被解码的数据和结构体字段的映射?
比如说给定一个JSON键“Foo”,Unmarshal将遍历目标结构的字段以查找(按以下规则顺序查找)

  • 带有”Foo”tag的导出字段(结构体tags参考Go规范
  • 导出字段名是”Foo”的
  • 导出字段名是”FOO”、”FoO”或者不区分大小写的”Foo”字段

当JSON数据的结构与Go类型不完全匹配时会发生什么?

1
2
3
b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal函数将只解码它可以在目标类型中找到的字段。在这个例子中,只有m的”Name”字段被映射,”Food”字段被忽略掉了。当需要在一个很大的JSON数据里pick指定的一些字段时,这个特性非常有用。这也意味着目标结构中任何未报告的字段不会受到Unmarshal函数的影响,不会解码在结构体中不存在的字段。

如果事先不知道JSON数据的结构该怎么办呢?

通过interface{}解析JSON

interface{}(空接口)类型,零个方法。每个Go类型至少实现了零个方法,因此满足空接口。

空接口用作通用容器类型:

1
2
3
4
var i interface{}
i = "a string"
i = 2011
i = 2.777

类型断言可以获取底层具体类型:

1
2
r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

另外,如果底层类型不确定,类型选择可以检查:

1
2
3
4
5
6
7
8
9
10
11
switch v := i.(type) {
case int:
fmt.Println("twice i is", v*2)
case float64:
fmt.Println("the reciprocal of i is", 1/v)
case string:
h := len(v) / 2
fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
// i isn't one of the types above
}

json包用map[string]interface{}和[]interface{}来存储JSON数据的对象和数组。
它会将任何有效的JSON blob解码到一个空接口上。
默认的转换规则是:

  • bool 对应JSON booleans
  • float64 对应JSON numbers
  • strings 对应JSON strings
  • nil 对应JSON null

Decoding arbitrary data

解码任意数据

假设存在变量b中的JSON数据如下:

1
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

不需要知道JSON数据结构的情况下,我们可以用Unmarshal函数解码到空接口上(interface{}):

1
2
var f interface{}
err := json.Unmarshal(b, &f)

此时,f中的Go值将是一个map,其键是字符串,其值本身存储为空接口值:

1
2
3
4
5
6
7
8
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}

要访问这些数据,我们可以使用类型断言来访问f的底层map[string]interface{}

1
m := f.(map[string]interface{})

然后使用range语句迭代map,并使用类型选择来访问其值的具体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case float64:
fmt.Println(k, "is float64", vv)
case []interface{}:
fmt.Println(k, "is an array")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}

通过这种方式,既保证了类型安全,又可以处理未知的JSON数据。

Reference Types

让我们定义一个Go类型来包含上面例子的结构体:

1
2
3
4
5
6
7
8
type FamilyMember struct {
Name string
Age int
Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

正如期望中的一样,数据被解码到FamilyMember,但如果我们仔细观察,就会发现发生了一件了不起的事情。
我们先声明了一个FamilyMeber类型的变量m,然后获取它的指针地址去Unmarshal,但是此时m中的Parents字段是一个nil切片。为了填充Parents字段,Unmarshal函数会在底层分配一个新的切片。这是典型的引用类型(points,slice和maps)解码的工作原理。

考虑将数据解码到如下数据结构:

1
2
3
type Foo struct{
Bar *Bar
}

如果在JSON数据里有一个”Bar”字段,Unmarshal函数会分配一个新Bar并填充它。如果不存在,Bar字段将会被当作空指针。

由此产生了一个有用的模式:如果你的应用程序接收到一些不同的消息类型,你可以定义”receiver”结构,如下:

1
2
3
4
type IncomingMessage struct{
Cmd *Command
Msg *Message
}

发送方可以填充JSON对象的顶级Cmd字段或者Msg字段,这取决于它们想要通信的消息类型。
在将JSON解码为IncomingMessage结构时,Unmarshal将只分配JSON数据中存在的数据结构。为了知道要处理哪些消息,程序员只需测试Cmd或Msg是否为nil。

Streaming Encoders and Decoders

json包提供了Decoder和Encoder类型,以支持读取和写入json数据流的常见操作。
NewDecoder和NewEncoder函数对io.Reader和io.Writer接口类型进行了封装。

1
2
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(r io.Writer) *Encoder

这里提供了一个代码demo来演示如何从标准输入里读取JSON对象,删除JSON对象里字段名非Name的字段,并输出到标准输出里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

func main() {
dec := json.NewDecoder(os.Stdin)
enc := json.NewEncoder(os.Stdout)
for {
var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
}

for k := range v {
if k != "Name" {
delete(v, k)
}
}

if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}

由于Readers和Writers的普遍存在,被很多Go类型所实现,所以Encoder和Decoder可以在广泛的场景中使用,例如读写http连接、WebSockets或文件。


Go里的json包是借助于反射来实现的,反射又是通过接口来实现的,所以学好接口很重要,找时间分析一下interface的源码。