JSON 和 Go (Go Blog 翻译)

原文地址: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

转载于:https://my.oschina.net/dokia/blog/1841675

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值