Go语言从入门到精通 -【web项目实战篇】- Json详解

本节核心内容

  • 介绍GoLang自带的json包的核心功能方法
  • 介绍如何利用Tag对Json结构体实现更多的控制
  • 介绍Json的编码器和解码器
  • 介绍如何解决复合结构体的数据读取问题
  • 介绍了开发中一些常见问题和解决方案
  • 介绍了比原生json包更快的json解析库

本小节视频教程和代码:百度网盘

可先下载视频和源码到本地,边看视频边结合源码理解后续内容,边学边练。

简介

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,因为易读性、机器容易处理而变得流行。

JSON 语言定义的内容非常简洁,主要分为三种类型:对象(object)、数组(array)和基本类型(value)。基本类型(value)包括:

  • string 字符串,双引号括起来的 unciode 字符序列
  • number 数字,可以是整数,也可以是浮点数,但是不支持八进制和十六进制表示的数字
  • true,false 真值和假值,一般对应语言中的 bool 类型
  • null 空值,对应于语言中的空指针等

数组(array)就是方括号括[]起来的任意值的序列,中间以逗号 , 隔开。

对象(object)是一系列无序的键值组合,键必须是字符串,键值对之间以逗号 , 隔开,键和值以冒号 : 隔开。数组和对象中的值都可以是嵌套的。

JSON 官网 有非常易懂的图示,进一步了解可以移步。

JSON 不依赖于任何具体的语言,但是和大多数 C 家族的编程语言数据结构特别相似,所以 JSON 成了多语言之间数据交换的流行格式。Go 语言也不例外,标准库 encoding/json 就是专门处理 JSON 转换的。

这篇文章就专门介绍 Go 语言中怎么和 JSON 打交道,常用的模式以及需要注意的事项。

使用

Golang 的 encoding/json 库已经提供了很好的封装,可以让我们很方便地进行 JSON 数据的转换。

Go 语言中数据结构和 JSON 类型的对应关系如下表:

GOLANG 类型JSON 类型注意事项
boolJSON booleans
浮点数、整数JSON numbers
字符串JSON strings字符串会转换成 UTF-8 进行输出,无法转换的会打印对应的 unicode 值。

而且为了防止浏览器把 json 输出当做 html, “<”、”>” 以及 “&”

会被转义为 “\u003c”、”\u003e” 和 “\u0026”。

array,sliceJSON arrays[]byte 会被转换为 base64 字符串,nil slice 会被转换为 JSON null
structJSON objects只有导出的字段(以大写字母开头)才会在输出中

NOTE:Go 语言中一些特殊的类型,比如 Channel、complex、function 是不能被解析成 JSON 的。

Encode 和 Decode

json 中提供的处理 json 的标准包是 encoding/json,它为我们提供了用于数据结构和 JSON 字符串互相转换的两个方法:

Marshal

要把 golang 的数据结构转换成 JSON 字符串(encode),可以使用 Marshal函数:

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

示例代码如下:

package main

import (
    "encoding/json"
    "fmt"
)
type Animal struct {
    Name  string `json:"name"`
    Order string `json:"order"`
}
func main() {
    var animals []Animal
    animals = append(animals, Animal{Name: "Platypus", Order: "Monotremata"})
    animals = append(animals, Animal{Name: "Quoll", Order: "Dasyuromorphia"})

    jsonStr, err := json.Marshal(animals)
    if err != nil {
        fmt.Println("error:", err)
    }

    fmt.Println(string(jsonStr))
}

运行后,输出结果:

[{"name":"Platypus","order":"Monotremata"},{"name":"Quoll","order":"Dasyuromorphia"}]

Unmarshal

要把 JSON 数据转换成 Go 类型的值(Decode), 可以使用 json.Unmarshal。它的定义是这样的:

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

data 中存放的是 JSON 值,v 会存放解析后的数据,所以必须是指针,可以保证函数中做的修改能保存下来。

已知解析类型

示例代码如下:

package main

import (
    "encoding/json"
    "fmt"
)
type Animal struct {
    Name  string
    Order string
}
func main() {
    var jsonBlob = []byte(`[
        {"Name": "Platypus", "Order": "Monotremata"},
        {"Name": "Quoll",    "Order": "Dasyuromorphia"}
    ]`)

    var animals []Animal
    err := json.Unmarshal(jsonBlob, &animals)
    if err != nil {
        fmt.Println("error:", err)
    }
    fmt.Printf("%+v", animals)
}

运行后,输出结果:[{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]

可以看出,结构体字段名与 JSON 里的 KEY 一一对应.

例如 JSON 中的 KEY 是 Name,那么怎么找对应的字段呢?

  • 首先查找 tag 含有 Name 的可导出的 struct 字段(首字母大写)

  • 其次查找字段名是 Name 的导出字段

  • 最后查找类似 NAME 或者 NAmE 等这样的除了首字母之外其他大小写不敏感的导出字段

注意:能够被赋值的字段必须是可导出字段!!

同时 JSON 解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的 JSON 数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。

未知解析类型

上面的解析过程有一个假设——你要事先知道要解析的 JSON 内容格式,然后定义好对应的数据结构。如果你不知道要解析的内容呢?

Go 提供了 interface{} 的格式,这个接口没有限定任何的方法,因此所有的类型都是满足这个接口的。在解析 JSON 的时候,任意动态的内容都可以解析成 interface{}。

在解析的过程中,我们可以使用断言(type assertion)来进行判断接受的数据为何种数据类型

package main

import (
        "encoding/json"
        "fmt"
)

func main() {
        var f interface{}
        b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
        json.Unmarshal(b, &f)
        for k, v := range f.(map[string]interface{}) {
                switch vv := v.(type) {
                case string:
                        fmt.Println(k, "is string", vv)
                case int:
                        fmt.Println(k, "is int ", vv)
                case float64:
                        fmt.Println(k, "is float64 ", vv)
                case []interface{}:
                        fmt.Println(k, "is array:")
                        for i, j := range vv {
                                fmt.Println(i, j)
                        }
                }
        }
}

运行结果:

Name is string Wednesday
Age is float64  6
Parents is array:
0 Gomez
1 Morticia

更多控制:Tag

Golang中可以为结构体的字段添加tag,这类似于Java中为类的属性添加的注解,Golang本身的encoding/json包解析json使用了tag。

我们在定义 struct 字段的时候,可以通过在字段后面添加 tag,来控制 encode/decode 的过程:是否要 decode/encode 某个字段,JSON 中的字段名称是什么。

json tag 有很多值可以取,同时有着不同的含义,比如:

  • -:不要解析这个字段,表示该字段不会输出到 JSON

  • omitempty 当字段为空(默认值)时,不要解析这个字段。比如 false、0、nil、长度为 0 的 array,map,slice,string,就不会输出到JSON 串中

  • FieldName,当解析 json 的时候,使用这个名字

  • ,string当字段类型是 bool, string, int, int64 等,而 tag 中带有该选项时,那么该字段在输出到 JSON 时,会把该字段对应的值转换成 JSON 字符串.

示例:

// 解析的时候忽略该字段。默认情况下会解析这个字段,因为它是大写字母开头的
Field int   `json:"-"`

// 解析(encode/decode) 的时候,使用 `other_name`,而不是 `Field`
Field int   `json:"other_name"`

// 解析的时候使用 `other_name`,如果struct 中这个值为空,就忽略它
Field int   `json:"other_name,omitempty"`


// 解析的时候会将接受到的字符串类型转为int类型
Field int   `json:"other_name,string"`

自定义解析方法

如果希望自己控制怎么解析成 JSON,或者把 JSON 解析成自定义的类型,只需要实现对应的接口(interface)。encoding/json 提供了两个接口:Marshaler 和 Unmarshaler:

// Marshaler 接口定义了怎么把某个类型 encode 成 JSON 数据
type Marshaler interface {
        MarshalJSON() ([]byte, error)
}

// Unmarshaler 接口定义了怎么把 JSON 数据 decode 成特定的类型数据。如果后续还要使用 JSON 数据,必须把数据拷贝一份
type Unmarshaler interface {
        UnmarshalJSON([]byte) error
}

示例代码:

package main

import (
        "bytes"
        "fmt"
)

// UnmarshalJSON _
func (m *Mail) UnmarshalJSON(data []byte) error {
        // 这里简单演示一下,简单判断即可
        if !bytes.Contains(data, []byte("@")) {
                return fmt.Errorf("mail format error")
        }
        m.Value = string(data)
        fmt.Printf("Correct mailbox format\n")
        return nil
}

// UnmarshalJSON _
func (m *Mail) MarshalJSON() (data []byte, err error) {
        if m != nil {
                data = []byte(m.Value)
        }
        return
}

// UnmarshalJSON _
func (p *Phone) UnmarshalJSON(data []byte) error {
        // 这里简单演示一下,简单判断即可
        if len(data) != 11 {
                return fmt.Errorf("phone format error")
        }
        p.Value = string(data)
        fmt.Printf("Correct phone format\n")
        return nil
}

// UnmarshalJSON _
func (p *Phone) MarshalJSON() (data []byte, err error) {
        if p != nil {
                data = []byte(p.Value)
        }
        return
}

// UserRequest _
type UserRequest struct {
        Name  string
        Mail  Mail
        Phone Phone
}
// Phone _
type Phone struct {
        Value string
}

// Mail _
type Mail struct {
        Value string
}
func main() {
        user := UserRequest{}
        user.Name = "Jack"

        var err error
        err = user.Mail.UnmarshalJSON([]byte("yangshiyu@x.com"))
        if nil != err {
                fmt.Println(err)
        }
        err = user.Phone.UnmarshalJSON([]byte("18900001111"))
        if nil != err {
                fmt.Println(err)
        }
        fmt.Printf("%v的邮箱为:%s手机号为%s",user.Name,user.Mail.Value,user.Phone.Value)
}

Json的编码器和解码器

json包提供了解码器和编码器类型,以支持读取和写入json数据流的常见操作。在该包中使用NewDecoderNewEncoder函数包装io。

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

在控制台分别输入:

{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}
{"Age":6,"Parents":["Gomez","Morticia"]}

运行结果为:

{"Name":"Wednesday"}
{}

由于读写器和编码器的普遍性,这些编码器和解码器类型可用于广泛的场景,例如对HTTP连接、WebSockets或文件的读写。

推荐的 json 解析库

jsoniter(json-iterator)是一款快且灵活的 JSON 解析器,同时提供 Java 和 Go 两个版本。从 dsljson 和 jsonparser 借鉴了大量代码。

jsoniter 的 Golang 版本可以比标准库(encoding/json)快 6 倍之多,而且这个性能是在不使用代码生成的前提下获得的。

可以使用 go get github.com/json-iterator/go 进行获取,完全兼容标准库的 MarshalUnmarshal方法。

使用时导入 github.com/json-iterator/go 代替标准库,基本用法如下:

jsoniter.Marshal(&data)
jsoniter.Unmarshal(input, &data)

性能测试代码:

package main

import (
        "testing"
        "github.com/json-iterator/go"
        "encoding/json"
)

var testString = `{"Name": "Platypus", "Order": "Monotremata"}`

func BenchmarkJsoniter(b *testing.B) {
        var animal Animal
        for i := 0; i < b.N; i++ {
                var err error
                var jsonBlob = []byte(testString)

                err = jsoniter.Unmarshal(jsonBlob, &animal)

                if err != nil {
                        b.Log("error:%v\n", err)
                }
        }
}

func BenchmarkJson(b *testing.B)  {
        var animal Animal

        for i := 0; i < b.N; i++ {
                var err error
                var jsonBlob = []byte(testString)

                err = json.Unmarshal(jsonBlob, &animal)
                if err != nil {
                        b.Log("error:%v\n", err)
                }
        }
}

使用gobench test运行测试结果如下:

goos: windows
goarch: amd64
pkg: test
BenchmarkJsoniter-8      3000000               399 ns/op
BenchmarkJson-8          1000000              1089 ns/op
PASS

开发中的一些常见问题

复合结构的解析

在开发中,我们可能常常面临着需要定义一些复合结构体,而如何即将接受到的json字符串转为结构体对象就成为了一件头疼的事儿,下面就会对于这种情景进行一个解答,示例代码如下:

package main

import (
        "fmt"
        "encoding/json"
)

type Car struct {
        Name  string
        Engine  Engine  //发动机
        Tire Tire       //轮胎
}

// Engine _
type Engine struct {
        Value string
}
// Tire _
type Tire struct {
        Value string
}


func main() {
        var str=[]byte(`{"Name":"奔驰","Engine":{"Value":"法拉利"},"Tire":{"Value":"米其林"}}`)
        car :=Car{}
        var engine Engine
        var tire Tire
        json.Unmarshal(str,&struct {
                *Engine
                *Tire
                *Car
        }{&engine,&tire,&car})
        fmt.Println(car)
        fmt.Printf("小明从小红的%v车上卸下了%v牌的发动机用来改造他的奥迪,连%v轮胎都不放过,真实孤终生啊!",car.Name,car.Engine.Value,car.Tire.Value)
}

Unmarshal 精度问题

golang使用json.Unmarshal的时候,有些数字类型的数据会默认转为float64,而一些数据因其比较大,导致输出的时候会出现数据与原数据不等的现象,解决办法是,将此数据类型变为json.Number,如下:

package main

import (
        "encoding/json"
        "fmt"
)

type MyData struct {
        Nid json.Number `json:"nid"`
}

func main() {
        var testJson = `{"nid":114420234065740369922}`
        var data MyData

        json.Unmarshal([]byte(testJson), &data)

        fmt.Println(data.Nid.String())
}

json.Number一共有三个方法,分别是:

  • String() 返回number的字符串
  • Float64() 返回number的64位的浮点类型
  • Int64() 返回number的64位整数类型

小结

本小节主要讲解了GoLang自带的encoding/json的几个主要序列化和反序列化方法以及如何自定义Json字符串,并在篇中给出了Json在定义时的一些方式和开发中的一些注意事项,最后给推荐了性能很棒的插件Jsoniter,以及性能对比。