介绍

JSON(JavaScript Object Notation)是一种简单的数据交换格式,语法上和JavaScript非常类似。它广泛应用于web后端与前端JavaScript程序的通信,当然在很多其他场合也非常常用。下面我们先简单介绍一下JSON语法。

JSON语法规则

JSON语法是JavaScript对象表示语法的子集。

  • 数据在“名称(key)/值”对中。比如"firstName" : "John".
  • 数据由逗号分隔。
  • 花括号保存对象。
  • 方括号保存数组。

JSON值

JSON的值可以是:

  • 数字(整数或浮点数)
  • 字符串(在双引号中)
  • 逻辑值(true或false)
  • 数组(在方括号中)
  • 对象(在花括号中)
  • null

JSON对象

JSON对象在花括号中书写,对象可以包含多个名称/值对:

{ "fistName":"John", "lastName":"Doe" }

JSON数组

JSON数组在方括号中书写,数组可包含多个对象:

{
    "employee": [
        { "fistName":"John", "lastName":"Doe" },
        { "firstName":"Anna" , "lastName":"Smith" },
        { "firstName":"Peter" , "lastName":"Jones" }
    ]
}

更多关于JSON信息可访问json.org。下面我们来看Go标准库提供的用于处理JSON格式的数据json包。

编码

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

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

比如有一个Go结构体Message和一个它的实例m:

type Message struct {
    Name string    `json:"name"`
    Body string
    Time int64
}

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中有如下规约:

  • JSON对象的key只能是string类型;比如我们要编码Go的Map类型的话,那这个map就必须是map[string]T(T是Go的json包里面支持的任意类型)形式的。NB:最新的Go文档中JSON的key可以为string类型或者整数类型或者任何实现encoding.TextMarshaler的类型。
  • Channel、复数和函数类型不能被编码。
  • 指针将被编码为它指向的值,如果是nil的话就编码为NULL。
  • 对于结构体类型,标准库中的json包只能处理导出的字段(大写开头的字段)。一些第三方的json包没有这个限制。

解码

我们可以使用Unmarshal函数来解码JSON数据:

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”的字段(比如Message中的name就是Name字段的标签)
  • 导出的名称为“Foo”的字段
  • 导出的名字为“FOO”、“FoO”等其他大小写不敏感的匹配“Foo”的字段

那如果像下面这样JSON数据中的字段与Go类型的不完全匹配会怎么样?

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

对于这种情况,Unmarshal函数只会解码能匹配上的字段,其他的不会受到任何影响。比如此例中,只会解码“Name”字段,“Food”字段将被忽略。当我们只想获取一个很大的JSON数据中的某一些字段时,该特性是非常有用的。但是如果我们事先不知道我们的JSON数据的结构怎么办呢?接着看下一节。

使用interface{}的通用JSON

之前在介绍Go的时候就提过,空接口interface{}可以接受任意类型的数据。所以我们可以使用Go的type assertiontype switch两个特性来获取未知数据背后的具体数据类型(如果对这两个特性或者空接口不清楚,请看我之前的博客《Go程序设计语言》要点总结——接口):

// 使用type assertion来访问具体类型
r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

// 如果底层类型未知,可以使用type switch来确定其类型
switch v := i.(type) {
case int:
    fmt.Println("integer")
case float64:
    fmt.Println("float64")
case string:
    fmt.Println("string")
default:
    fmt.Println("Unkonwn type")

标准库的json包使用map[string]interface{}[]interface{}值来存储任意的JSON对象和数组;所以Unmarshal函数可以将任意合法的JSON对象解码到interface{}中。具体Go类型与JSON类型对应关系如下:

  • bool对应boolean
  • float64对应JSON的数字
  • string对应JSON的字符串
  • nil对应JSON的null

下一节我们看个例子。

解码任意数据

考虑下面存储于b中的JSON数据:

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

我们无需知道b的数据结构,可以直接使用Unmarshal函数将其解码到空接口interface{}里:

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

此时f的值将是一个map,这个map的key是string类型,value是interface{}类型:

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

我们可以使用之前说的方法,通过type assertion和type switch来访问和迭代获取f中的数据:

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

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case int:
        fmt.Println(k, "is int", 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")
    }
}

引用类型

我们定义一个Go类型包含前面例子中的数据:

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

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

我们发现上面代码可以正常解码。但是我们仔细观察发现当使用var声明FamilyMember类型的变量m时,Parents字段值为nil,但是解析数据时却并没有panic(正常往值为nil的引用类型中插入数据会引发panic)。这是因为Unmarshal函数内部为了解码Parents字段,而为其分配了内存。Unmarshal函数这个特性适用于Go中所有的引用类型——指针、Slice、Map。当然,如果数据中没有引用类型的数据,那引用类型依旧为nil。比如对于上面结构体,如果b中没有Parents的数据,那Unmarshal之后,Parents字段为nil。

这个特性有个非常实用的应用场景:比如我们的程序可以接收几种不同类型的消息,比如在后端编程中,经常会接收到控制消息和数据消息,此时我们可以在接收端定义如下结构体:

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

然后发送端程序可以根据自己想发送的消息类型在发送端的JSON格式数据的顶层实现任意一个类型的消息。而接收端我们就可以使用Unmarshal函数来解析消息,我们可以根据哪个字段不为nil来判断发送端发送的消息类型。

JSON流编码和解码

json包里面提供了两个通用的用于流式的操作JSON数据的类型:DecoderEncoder。然后NewDecoder和NewEncoder两个函数封装了io.Readerio.Writer接口类型。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(r io.Writer) *Encoder

下面的例子程序实现从标准输入读入JSON对象,然后去掉除所有非"Name"字段,然后写到标准输出:

package main

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

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类型是非常普遍的,所以这种用法可以用在很多场景,比如HTTP连接、WebSocket、文件等。

本文参考自:

  1. https://blog.golang.org/json-and-go
  2. http://www.w3school.com.cn/json/json_syntax.asp
  3. https://golang.org/pkg/encoding/json/