原文地址:JSON and Go。
引言
JSON (JavaScript Object Notation) 是一种简单的数据交换格式。从语法上讲,它类似于JavaScript的对象和列表。它常用于 web 后端程序和浏览器中运行的 JavaScript 程序的通信上。它在其他方面也有很广泛的使用。它的官网,json.org,提供了完美清晰且准确的定义标准。
Go 提供了 json 包,我们可以使用它来很容易地在 Go 程序中读取和写入 JSON 数据。
编码
我们使用 Marshal 方法来编码 JSON 数据:
func Marshal(v interface{}) ([]byte, error)
已知 Go 的结构类型:Message:
type Message struct {
Name string
Body string
Time int64
}
下面是 Message 的一个实例:
m := Message{"Alice", "Hello", 1294706395881547000}
我们可以使用 json.Marshal 把 m 编码成 JSON 类型的数据:
b, err := json.Marshal(m)
如果一切正常,那么 err 将会为 nil,b 会是 []byte 类型的数据,其中包含着下面的 JSON 数据:
b == []byte(`{"Name": "Alice","Body":"Hello","Time": 1294706395881547000}`)
只有能被表示成合法 JSON 的数据结构才会被编码成功:
- JSON 对象只支持字符串作为键。如果想对一个 Go map 类型的数据编码,那么这个 map 一定要是 map[string]T 类型的。(T 是 json 包支持的任意 Go 类型)
- Channel、复数、以及函数类型不能被编码。
- 不支持有环的数据结构;它会使 Marshal 进入死循环。
- 指针会被按照它们指向的值进行编码。(如果指针是 nil,那么编码结果是 "null")
json 包只会访问结构体中外部可输出的字段 (即 exported 字段。这些字段需首字母大写)。因此结构体中只有外部可输出的字段才会被编码到 JSON 输出结果中。
解码
Unmarshal 方法用于解码 JSON 数据。
func Unmarshal(data []byte, v interface{}) error
首先我们需要创建一个存储解码数据的变量:
var m Message
然后调用 json.Unmarshal,传入 []byte 类型的 JSON 数据,以及一个指向 m 的指针:
err := json.Unmarshal(b, &m)
如果 b 包含合法的 JSON 数据,并且此数据与 m 结构相匹配,那么返回的 err 为 nil,且 b 中的数据会被存储在 m 中,就像下面的赋值操作:
m = Message{
Name: "Alice",
Body: "Hello",
Time: 1294706395881547000,
}
Unmarshal 是如何识别各字段的呢?对于一个给定的 JSON 键 “Foo”,Unmarshal 会查找目标结构体的字段去寻找 (按照以下顺序):
- 带有 “Foo” tag 的可输出字段 (tag 描述文档),
- 名称为 “Foo” 的可输出字段,或
- 名称为 “FOO”、“FoO” 或其他非大小写敏感匹配 “Foo” 的可输出字段。
如果 JSON 数据和 Go 结构体类型无法完全匹配呢?
b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)
Unmarshal 会只编码它能在结构体中找到的字段。在这个例子里,只有 Name 字段会被赋值,JSON 中的 Food 字段会被忽略。如果你想从一个大的 JSON 数据中只选取一小部分特定字段时,这个特性将会特别有用。它也意味着目标结构体中任何不可输出的字段都不会被 Unmarshal 所影响。
如果事前不知道 JSON 数据的结构,该怎么办呢?
使用 interface{} 的通用 JSON
interface{} (空接口) 类型描述了一个包含零个方法的接口。每个 Go 类型实现了至少零个方法,所以任意 Go 类型都满足空接口。
空接口可以充当一般容器类型:
var i interface{}
i = "a string"
i = 2011
i = 2.777
通过类型声明,我们可以访问到接口的的底层类型:
r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)
或者如果接口的底层类型未知,那么可以使用类型选择来判断类型:
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 type above
}
json 包使用 map[string]interface{} 和 []interface{} 值来存储任意 JSON 对象和数组;它很乐意把任意合法的 JSON 结构解码为一个空接口值。默认的 Go 类型映射为:
- bool 对应 JSON 布尔值 (true 和 false),
- float64 对应 JSON 数值 (整型值也映射为 Go float64),
- string 对应 JSON 字符串,
- nil 对应 JSON 空值 (null)。
解码任意数据
对于下面一个存储在变量 b 中的 JSON 数据:
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
如果事先不知道这个 JSON 数据的结构,我们可以使用 Unmarshal 把它解码到一个空接口中:
var f interface{}
err := json.Unmarshal(b, &f)
现在 f 将会是一个 map 类型的值,它的键是字符串类型,它的值是按照空接口类型存储的 JSON 各个值。解码过程等价于以下赋值操作:
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
我们可以使用类型声明来访问 f 的底层类型,即 map[string]interface{}:
m := f.(map[string]interface{})
然后我们可以使用 range 来遍历 map,然后使用类型选择来访问 map 的值:
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{}:
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 数据,同时保证类型安全。
引用类型
下面我们定义一个 Go 类型,用于存储之前例子中的数据:
type FamilyMember struct {
Name string
Age int
Parents []string
}
var m FamilyMember
err := json.Unmarshal(b, &m)
这个把 b 中的数据解码到 FamilyMember 类型的值中的操作运行正常,但是如果我们仔细观察的话,会发现一些比较值得注意的事情。在这里我们分配了一个 FamilyMember 类型的结构体,然后把指向这个结构体的指针提供给 Unmarshal,但是这时结构体的 Parents 字段是 nil 的切片。为了填充字段 Parents,Unmarshal 在函数内部分配了一个新的切片。这是 Unmarshal 处理引用类型 (指针、切片、以及 map) 的典型方式。
考虑一下把 JSON 数据解码到下面这个数据结构中:
type Foo struct {
Bar *Bar
}
如果 JSON 对象中有 Bar 字段,那么 Unmarshal 函数会分配一个新的 Bar 然后填充到结构体中。如果 JSON 对象中没有 Bar 字段,那么结构体中的 Bar 字段会是值为 nil 的指针。
利用这种特性,我们可以使用一种有用的模式:如果你的应用程序需要接受一些不同的消息类型,你可以把接受的数据类型定义成下面这样:
type IncomingMessage struct {
Cmd *Command
Msg *Message
}
发送端在发送消息时,会根据需要发送的通信类型,来填充 JSON 数据的 Cmd 和/或 Msg 字段。Unmarshal 函数把相应的 JSON 数据编码成 ImcomingMessage 结构体时,只会分配存在于 JSON 数据中的数据结构。在接收端,码农们只需要简单的判断 Cmd 或 Msg 是否为 nil,就能知道需要处理那种类型的消息了。
流式编码器和解码器
json 包提供 Decoder 和 Encoder 类型来支持对流式 JSON 数据常用的读写操作。NewDecoder 和 NewEncoder 方法包装了 io.Reader 和 io.Writer 接口类型。
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
下面的例子从标准输入读取一系列的 JSON 对象,删除每个对象中除了 Name 字段以外的字段,然后把对象写到标准输出去:
package main
import (
"encoding/json"
"log"
"os"
)
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)
return
}
for k := range v {
if k != "Name" {
delete(v, k)
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}
由于 Readers 和 Writers 的通用性,Encoder 和 Decoder 类型可以被应用于很多场景中,比如读写 HTTP 连接、WebSockets、或文件等。
参考
更多信息请参考 json 包文档。json 的使用用例请参考 jsonrpc 包。
-- 作者:Andrew Gerrand