JSON
JavaScript 对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON 并不是唯一的一个标准协议。 XML(§7.14)、ASN.1 和 Google 的 Protocol Buffers 都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON 是应用最广泛的一个。
Go 语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的 encoding/json、encoding/xml、encoding/asn1 等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的 API 接口。本节,我们将对重要的 encoding/json 包的用法做个概述。
JSON 是对 JavaScript 中各种类型的值——字符串、数字、布尔值和对象—— Unicode 本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和 map 等聚合数据类型。
基本的 JSON 类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的 Unicode 字符序列,支持和 Go 语言类似的反斜杠转义特性,不过 JSON 使用的是 \Uhhhh
转义数字来表示一个 UTF-16 编码(译注:UTF-16 和 UTF-8 一样是一种变长的编码,有些 Unicode 码点较大的字符需要用 4 个字节表示;而且 UTF-16 还有大端和小端的问题),而不是 Go 语言的 rune 类型。
这些基础类型可以通过 JSON 的数组和对象类型进行递归组合。一个 JSON 数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个 JSON 数组可以用于编码 Go 语言的数组和 slice 。一个 JSON 对象是一个字符串到值的映射,写成一系列的 name:value 对形式,用花括号包含并以逗号分隔;JSON 的对象类型可以用于编码 Go 语言的 map 类型(key 类型是字符串)和结构体。例如:
boolean true
number -273.15
string "She said \"Hello, BF\""
array ["gold", "silver", "bronze"]
object {"year": 1980,
"event": "archery",
"medals": ["gold", "silver", "bronze"]}
json
考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的 Movie 数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year 和 Color 成员后面的字符串面值是结构体成员 Tag ;我们稍后会解释它的作用。)
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/movie/main.go[]
go
这样的数据结构特别适合 JSON 格式,并且在两者之间相互转换也很容易。将一个 Go 语言中类似 movies 的结构体 slice 转为 JSON 的过程叫编组(marshaling)。编组通过调用 json.Marshal 函数完成:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/movie/main.go[]
go
Marshal 函数返回一个编码后的字节 slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示:
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]
bash
这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。为了生成便于阅读的格式,另一个 json.MarshalIndent 函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/movie/main.go[]
go
上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符):
[
{
"Title": "Casablanca",
"released": 1942,
"Actors": [
"Humphrey Bogart",
"Ingrid Bergman"
]
},
{
"Title": "Cool Hand Luke",
"released": 1967,
"color": true,
"Actors": [
"Paul Newman"
]
},
{
"Title": "Bullitt",
"released": 1968,
"color": true,
"Actors": [
"Steve McQueen",
"Jacqueline Bisset"
]
}
]
json
在编码时,默认使用 Go 语言结构体的成员名字作为 JSON 的对象(通过 reflect 反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。
细心的读者可能已经注意到,其中 Year 名字的成员在编码后变成了 released,还有 Color 成员编码后变成了小写字母开头的 color 。这是因为结构体成员 Tag 所导致的。一个结构体成员 Tag 是和在编译阶段关联到该成员的元信息字符串:
Year int `json:"released"`
Color bool `json:"color,omitempty"`
bash
结构体的成员 Tag 可以是任意的字符串面值,但是通常是一系列用空格分隔的 key:"value" 键值对序列;因为值中含有双引号字符,因此成员 Tag 一般用原生字符串面值的形式书写。json 开头键名对应的值用于控制 encoding/json 包的编码和解码的行为,并且 encoding/… 下面其它的包也遵循这个约定。成员 Tag 中 json 对应值的第一部分用于指定 JSON 对象的名字,比如将 Go 语言中的 TotalCount 成员对应到 JSON 中的 total_count 对象。Color 成员的 Tag 还带了一个额外的 omitempty 选项,表示当 Go 语言结构体成员为空或零值时不生成该 JSON 对象(这里 false 为零值)。果然,Casablanca 是一个黑白电影,并没有输出 Color 成员。
编码的逆操作是解码,对应将 JSON 数据解码为 Go 语言的数据结构,Go 语言中一般叫 unmarshaling ,通过 json.Unmarshal 函数完成。下面的代码将 JSON 格式的电影数据解码为一个结构体 slice ,结构体中只有 Title 成员。通过定义合适的 Go 语言数据结构,我们可以选择性地解码 JSON 中感兴趣的成员。当 Unmarshal 函数调用返回,slice 将被只含有 Title 信息的值填充,其它 JSON 成员将被忽略。
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
go
许多 web 服务都提供 JSON 接口,通过 HTTP 接口发送 JSON 格式请求并返回 JSON 格式的信息。为了说明这一点,我们通过 Github 的 issue 查询服务来演示类似的用法。首先,我们要定义合适的类型和常量:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/github/github.go[]
go
和前面一样,即使对应的 JSON 对象名是小写字母,每个结构体的成员名也是声明为大写字母开头的。因为有些 JSON 成员名字和 Go 结构体成员名字并不相同,因此需要 Go 语言结构体成员 Tag 来指定对应的 JSON 名字。同样,在解码的时候也需要做同样的处理,GitHub 服务返回的信息比我们定义的要多很多。
SearchIssues 函数发出一个 HTTP 请求,然后解码返回的 JSON 格式的结果。因为用户提供的查询条件可能包含类似 ?
和 &
之类的特殊字符,为了避免对 URL 造成冲突,我们用 url.QueryEscape 来对查询中的特殊字符进行转义操作。
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/github/search.go[]
go
在早些的例子中,我们使用了 json.Unmarshal 函数来将 JSON 格式的字符串解码为字节 slice 。但是这个例子中,我们使用了基于流式的解码器 json.Decoder ,它可以从一个输入流解码 JSON 数据,尽管这不是必须的。如您所料,还有一个针对输出流的 json.Encoder 编码对象。
我们调用 Decode 方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种,以一个固定宽度打印每个 issue ,但是在下一节我们将看到如何利用模板来输出复杂的格式。
Unresolved include directive in modules/ROOT/pages/ch4/ch4-05.adoc - include::example$/ch4/issues/main.go[]
go
通过命令行参数指定检索条件。下面的命令是查询 Go 语言项目中和 JSON 解码相关的问题,还有查询返回的结果:
$ go build gopl.io/ch4/issues
$ ./issues repo:golang/go is:open json decoder
13 issues:
#5680 eaigner encoding/json: set key converter on en/decoder
#6050 gopherbot encoding/json: provide tokenizer
#8658 gopherbot encoding/json: use bufio
#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal
#5901 rsc encoding/json: allow override type marshaling
#9812 klauspost encoding/json: string tag not symmetric
#7872 extempora encoding/json: Encoder internally buffers full output
#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin
#6716 gopherbot encoding/json: include field name in unmarshal error me
#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi
#6384 joeshaw encoding/json: encode precise floating point integers u
#6647 btracey x/tools/cmd/godoc: display type kind of each named type
#4237 gjemiller encoding/base64: URLEncoding padding is optional
bash
GitHub 的 Web 服务接口 https://developer.github.com/v3/ 包含了更多的特性。
练习 4.10: 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、超过一年。
练习 4.11: 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当必要的时候自动打开用户默认的编辑器用于输入文本信息。
练习 4.12: 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。
练习 4.13: 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。