[Go 入门] 第十九章 处理 JSON

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

JavaScript 对象表示法 (JAvaScript Object Notation, JSON) 是一种储存或交换的数据格式 , 本文内容:

JSON 简介

JSON 可以键值对的方式表示数据,也可以数组的方式表示数据。 JSON 最初是一个 JavaScript 子集,但它现在已独立于语言,实际上,大多数语言都支持 JSON 数据的编码和解码。 JSON 已成为互联网上储存和交换数据的事实标准。

如下是使用 GitHub API (GET https://api.github.com/users/:username) 获得的 JSON 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"login": "dirname",
"id": 32116910,
"node_id": "MDQ6VXNlcjMyMTE2OTEw",
"avatar_url": "https://avatars1.githubusercontent.com/u/32116910?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/dirname",
"html_url": "https://github.com/dirname",
"followers_url": "https://api.github.com/users/dirname/followers",
"following_url": "https://api.github.com/users/dirname/following{/other_user}",
"gists_url": "https://api.github.com/users/dirname/gists{/gist_id}",
"starred_url": "https://api.github.com/users/dirname/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dirname/subscriptions",
"organizations_url": "https://api.github.com/users/dirname/orgs",
"repos_url": "https://api.github.com/users/dirname/repos",
"events_url": "https://api.github.com/users/dirname/events{/privacy}",
"received_events_url": "https://api.github.com/users/dirname/received_events",
"type": "User",
"site_admin": false,
"name": "Ah !",
"company": null,
"blog": "https://blog.forgiveher.cn",
"location": "Shang Hai",
"email": null,
"hireable": null,
"bio": "Not Cav Empt just headache",
"public_repos": 14,
"public_gists": 0,
"followers": 10,
"following": 0,
"created_at": "2017-09-20T02:02:28Z",
"updated_at": "2020-01-23T16:56:20Z"
}

JSON 得以流行是因为它是一种人类能够看懂的灵活而且轻量级的数据格式。

使用 JSON API

近年来,互联网上出现了很多卓越的 JSON API。现在,通过数据交换平台的互联网,可获得大量有关任何主题的数据。 API 让程序员无需直接连接到数据库,就可以请求各种格式的数据并使用它们,这样的 API 包括:

  • 纽约市交通局:通过网络提供火车、汽车和地铁交通信息,以及自动扶梯状态信息
  • 英国广播公司:提供电视和广播节目播放时间表、分类细节和图片
  • Github:提供 Github 上的各种数据和信息
  • DarkSky:这是一个天气预报服务,通常比其他天气预报服务更准确

应用程序开发人员已使用很多这样的 API 来开发新颖而有趣的产品和服务。

在 Go 语言中使用 JSON

Go 语言非常适合用来创建和收发 JSON 的客户端和服务器。标准库提供了 encoding/json 包,可用于编码和解码 JSON 数据

编码意味着将数据转为编码后的格式。encoding/json 提供了函数 Marshal 可用于将 Go 数据编码为 JSON

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

import (
"encoding/json"
"fmt"
"log"
)

type Person struct {
Name string
Age int
Hobbies []string
}

func main() {
hobbies := []string{"Cycling", "Cheese", "Techno"}
p := Person{Name: "Buck", Age: 40, Hobbies: hobbies}
jsonByteData, err := json.Marshal(p)
if err != nil{
log.Fatal(err)
}
fmt.Println(string(jsonByteData))
}

执行后,将输出

1
{"Name":"Buck","Age":40,"Hobbies":["Cycling","Cheese","Techno"]}

虽然前面的示例成功地将数据转换成了 JSON 格式,但存在一个问题:在 JSON 数据中,所有键名都以大写字母开头,虽然 JSON 没有官方标准,但是约定了使用驼峰拼写法,因此,对于结构体,可以给其数据字段指定标签,对于带 JSON 标签的数据,将使用标签中的数据替换它

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

import (
"encoding/json"
"fmt"
"log"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
}

func main() {
hobbies := []string{"Cycling", "Cheese", "Techno"}
p := Person{Name: "Buck", Age: 40, Hobbies: hobbies}
jsonByteData, err := json.Marshal(p)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonByteData))
}

这样即输出:

1
{"name":"Buck","age":40,"hobbies":["Cycling","Cheese","Techno"]}

如果结构体的字段设置为空值,则编码 JSON 格式后,将包含 Go 语言零值规则指定的值

p := Person{}

1
{"name":"","age":0,"hobbies":null}

如果确实为空就忽略它,可添加 omitempty

1
2
3
4
5
type Person struct {
Name string `json:"name,omitempty"`
Age int `json:"age,omitempty"`
Hobbies []string `json:"hobbies,omitempty"`
}

这样如果使用 p := Person{} 则将输出:

1
{}

解码 JSON

JSON 解码也是一种常见的网络编程任务,收到的数据可能来自数据库、API调用或配置文件。原始 JSON 就是文本格式的数据。 Go 语言中可表示字符串。函数 Unmarshal 接收一个字节切片以及一个指定要将数据解码为何种格式的接口。根据数据是如何收到的,它可能是字节切片,也可能不是。如果不是,就必须先转换,再传递给函数

1
2
var jsonData = `{"name":"Buck","age":40,"hobbies":["Cycling","Cheese","Techno"]}`
jsonByteData := []byte(jsonData)

与编码格式一样,可以定义一个结构体,来指定将数据解码为何种格式

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

import (
"encoding/json"
"fmt"
"log"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
}

func main() {
var jsonData = `{"name":"Buck","age":40,"hobbies":["Cycling","Cheese","Techno"]}`
jsonByteData := []byte(jsonData)
p := Person{}
err := json.Unmarshal(jsonByteData, &p)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p)
}

执行后发现,JSON 已经被转换为 Person 结构体示例了:

1
{Name:Buck Age:40 Hobbies:[Cycling Cheese Techno]}

也可以使用接口,来解码 JSON

1
2
3
4
5
6
7
8
9
10
11
func main() {
m := make(map[string]interface{})
var jsonData = `{"name":"Buck","age":40,"hobbies":["Cycling","Cheese","Techno"]}`
jsonByteData := []byte(jsonData)
err := json.Unmarshal(jsonByteData, &m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", m)
fmt.Printf("%s", m["name"])
}

执行后,输出:

1
2
map[age:40 hobbies:[Cycling Cheese Techno] name:Buck]
Buck

映射数据类型

编码和解码 JSON 时,必须考虑 Go 和 JavaScript 表示数据类型的方式,这很重要,Go 是一种强类型语言,而 JavaScript 是一种弱类型语言,既不显式地声明变量的数据类型

在 JSON 中使用数据类型如下:

  • Boolean
  • Number
  • String
  • Array
  • Object
  • Null

JSON 数据类型与 Go 数据类型对应关系

JSON Go
Boolean bool
Number float64
String string
Array [ ]interface{}
Object map[string]interface{}
Null nil

创建用于编码和解码的结构体时必须对上述的数据类型的对应关系做到心中有数,因为如果数据类型不匹配,encoding/json 将会引发错误

处理通过 HTTP 收到的 JSON

在 Go 语言,通过 HTTP 请求获取 JSON 时,收到的数据为流而不是字符串或者字节切片。这种情况下 encoding/json 提供了函数 NewDecoder,这个函数接收一个 io.Reader,并返回一个 Decoder, 通过对返回的 Decoder 调用方法 Decode,可将数据转为接口

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

import (
"encoding/json"
"fmt"
"log"
"net/http"
)

func main() {
res, err := http.Get("https://api.github.com/users/dirname")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
m := make(map[string]interface{})
err = json.NewDecoder(res.Body).Decode(&m)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v", m)
}

执行将输出:

1
map[avatar_url:https://avatars1.githubusercontent.com/u/32116910?v=4 bio:Not Cav Empt just headache blog:https://blog.forgiveher.cn company:<nil> created_at:2017-09-20T02:02:28Z email:<nil> events_url:https://api.github.com/users/dirname/events{/privacy} followers:10 followers_url:https://api.github.com/users/dirname/followers following:0 following_url:https://api.github.com/users/dirname/following{/other_user} gists_url:https://api.github.com/users/dirname/gists{/gist_id} gravatar_id: hireable:<nil> html_url:https://github.com/dirname id:3.211691e+07 location:Shang Hai login:dirname name:Ah ! node_id:MDQ6VXNlcjMyMTE2OTEw organizations_url:https://api.github.com/users/dirname/orgs public_gists:0 public_repos:14 received_events_url:https://api.github.com/users/dirname/received_events repos_url:https://api.github.com/users/dirname/repos site_admin:false starred_url:https://api.github.com/users/dirname/starred{/owner}{/repo} subscriptions_url:https://api.github.com/users/dirname/subscriptions type:User updated_at:2020-01-23T16:56:20Z url:https://api.github.com/users/dirname]

转换为结构体:

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

import (
"encoding/json"
"fmt"
"log"
"net/http"
)

type User struct {
Name string `json:"name"`
Blog string `json:"blog"`
}

func main() {
res, err := http.Get("https://api.github.com/users/dirname")
if err != nil{
log.Fatal(err)
}
defer res.Body.Close()
m := User{}
err = json.NewDecoder(res.Body).Decode(&m)
if err != nil{
log.Fatal(err)
}
fmt.Printf("%+v", m)
}

执行将输出:

1
{Name:Ah ! Blog:https://blog.forgiveher.cn}

问题列表

  • 为编码和解码 JSON,必须创建结构体,还是会自动生成结构体

    是的,要解码或解码JSON,必须先创建结构体,虽然这样很繁琐,但是可让代码更健壮、容错能力更强

  • 为何所有的人都选择使用,而不是其他数据传输格式 (如 XML)

    JSON 更为轻量级,占用空间更少,并且灵活、易学富有表达力