Go 博客

JSON 和 Go

Andrew Gerrand
2011 年 1 月 25 日

引言

JSON(JavaScript 对象表示法)是一种简单的数据交换格式。在语法上,它类似于 JavaScript 的对象和列表。它最常用于 Web 后端与在浏览器中运行的 JavaScript 程序之间的通信,但它也用于许多其他地方。它的主页 json.org 提供了对该标准的清晰简洁的定义。

使用 json 包,可以轻松地从 Go 程序中读取和写入 JSON 数据。

编码

要编码 JSON 数据,我们使用 Marshal 函数。

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[string]T 的形式(其中 T 是 json 包支持的任何 Go 类型)。

  • 通道、复数和函数类型无法编码。

  • 不支持循环数据结构;它们会导致 Marshal 进入无限循环。

  • 指针将被编码为它们指向的值(如果指针为 nil,则为“null”)。

json 包仅访问结构体类型的导出字段(那些以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在 JSON 输出中。

解码

要解码 JSON 数据,我们使用 Unmarshal 函数。

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

我们必须首先创建一个用于存储解码数据的空间。

var m Message

并调用 json.Unmarshal,将 JSON 数据的 []byte 和指向 m 的指针传递给它。

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

如果 b 包含适合 m 的有效 JSON,则调用后 err 将为 nil,并且 b 中的数据将存储在结构体 m 中,就像赋值一样:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Unmarshal 如何确定存储解码数据的字段?对于给定的 JSON 键 "Foo"Unmarshal 将在目标结构体的字段中查找(按优先顺序):

  • 具有 "Foo" 标签的导出字段(有关结构体标签的更多信息,请参阅 Go 规范),

  • 名为 "Foo" 的导出字段,或

  • 名为 "FOO""FoO" 或其他不区分大小写的 "Foo" 匹配的导出字段。

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

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

Unmarshal 将仅解码它能在目标类型中找到的字段。在这种情况下,只有 m 的 Name 字段会被填充,Food 字段将被忽略。当您希望从大型 JSON 数据块中只提取几个特定字段时,此行为特别有用。这也意味着目标结构体中的任何未导出字段都不会受到 Unmarshal 的影响。

但是,如果您事先不知道 JSON 数据的结构,该怎么办?

泛用 JSON 与 interface

interface{}(空接口)类型描述了一个没有方法的接口。每个 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 types above
}

json 包使用 map[string]interface{}[]interface{} 值来存储任意 JSON 对象和数组;它会愉快地将任何有效的 JSON 数据块解组到普通的 interface{} 值中。默认的具体 Go 类型是:

  • bool 用于 JSON 布尔值,

  • float64 用于 JSON 数字,

  • string 用于 JSON 字符串,以及

  • nil 用于 JSON null。

解码任意数据

考虑此 JSON 数据,它存储在变量 b 中。

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

在不知道此数据结构的情况下,我们可以使用 Unmarshal 将其解码为 interface{} 值。

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

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

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

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

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

然后,我们可以使用 range 语句遍历映射,并使用类型开关将值作为其具体类型进行访问。

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 数据,同时仍然享受类型安全的优势。

引用类型

让我们定义一个 Go 类型来包含上一个示例中的数据。

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

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

将该数据解组到 FamilyMember 值中工作正常,但如果我们仔细观察,会发现一个了不起的事情发生了。通过 var 语句,我们分配了一个 FamilyMember 结构体,然后将该值的指针提供给 Unmarshal,但当时 Parents 字段是一个 nil 切片值。为了填充 Parents 字段,Unmarshal 在后台分配了一个新的切片。这通常是 Unmarshal 如何处理支持的引用类型(指针、切片和映射)。

考虑解组到此数据结构中。

type Foo struct {
    Bar *Bar
}

如果 JSON 对象中有一个 Bar 字段,Unmarshal 将分配一个新的 Bar 并填充它。如果没有,Bar 将保持为 nil 指针。

由此产生了一个有用的模式:如果您有一个应用程序接收几种不同的消息类型,您可以定义一个“接收器”结构,如下所示:

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

发送方可以根据他们想要通信的消息类型,填充顶层 JSON 对象的 Cmd 字段和/或 Msg 字段。Unmarshal 在将 JSON 解码为 IncomingMessage 结构体时,只会分配 JSON 数据中存在的数据结构。为了知道要处理哪些消息,程序员只需测试 CmdMsg 是否不为 nil

流式编码器和解码器

json 包提供了 DecoderEncoder 类型来支持读取和写入 JSON 数据流的常见操作。NewDecoderNewEncoder 函数包装了 io.Readerio.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)
        }
    }
}

由于 Reader 和 Writer 的普遍性,这些 EncoderDecoder 类型可以在广泛的场景中使用,例如读写 HTTP 连接、WebSockets 或文件。

参考

有关更多信息,请参阅 json 包文档。有关 json 的示例用法,请参阅 jsonrpc 包 的源代码文件。

下一篇文章: Go 变得更加稳定
上一篇文章: Go 切片:用法和内部
博客索引