JSON和Go

本文翻译自《JSON and Go》。

Andrew Gerrand

2011/01/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将为nilb将是包含此JSON数据的[]byte

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

只有可以表示为有效JSON的数据结构才可以被编码:

  • JSON对象只支持字符串作为键;要编码Go的map类型,它必须是map[string]T格式(其中T是json包支持的任何Go类型)。
  • 无法对通道、复数和函数类型进行编码。
  • 不支持循环数据结构;他们会使Marshal陷入无限循环。
  • 指针将被编码为它们指向的值(如果指针为空,则编码为“null”)。

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

解码

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

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

我们必须首先创建一个存储解码数据的变量:

var m Message

并调用json.Unmarshal,将一个[]byte的JSON数据和一个指向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”标签(tag)的导出(公有)字段(有关结构体标签的更多信息,请参阅Go语言规范),
  • 名为“Foo”的导出字段,或
  • 其他“Foo”单词的不区分大小写的匹配项,例如名为“FOO”或“FoO”的导出字段。

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

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

Unmarshal将只解码它可以在目标类型中找到的字段。在这种情况下,只会填充mName字段,而忽略Food字段。当你希望从大型JSON数据中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构体中任何未导出(私有)的字段都不会受到Unmarshal的影响。

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

带接口的通用JSON

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语句来确定类型:

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的类型不是以上类型中的一种
}

json包使用map[string]interface{}[]interface{}值来存储任意JSON对象或数组;它会愉快地将任何有效的JSON blob解码为一个普通的interface{}值。interface{}值默认使用的底层Go类型是:

  • bool用于JSON布尔值,
  • float64用于JSON数字值,
  • string用于JSON字符串值,以及
  • nil用于JSON的null(空值)。

解码任意数据

考虑这个存储在变量b中的JSON数据:

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

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

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

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

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

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

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

然后我们可以使用range语句遍历这个map,并使用switch语句来判断其值的具体类型:

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)
        }
    }
}

由于io.Readerio.Writer的广泛使用,这些DecoderEncoder类型可以用于广泛的场景,例如读取和写入HTTP连接、WebSocket或文件等。

参考

有关详细信息,请参阅json包的文档。有关json的示例用法,请参阅jsonrpc包的源文件。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注