本节核心内容
- 介绍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 类型 | 注意事项 |
---|---|---|
bool | JSON booleans | |
浮点数、整数 | JSON numbers | |
字符串 | JSON strings | 字符串会转换成 UTF-8 进行输出,无法转换的会打印对应的 unicode 值。 而且为了防止浏览器把 json 输出当做 html, “<”、”>” 以及 “&” 会被转义为 “\u003c”、”\u003e” 和 “\u0026”。 |
array,slice | JSON arrays | []byte 会被转换为 base64 字符串,nil slice 会被转换为 JSON null |
struct | JSON 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("[email protected]"))
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数据流的常见操作。在该包中使用NewDecoder
和NewEncoder
函数包装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
进行获取,完全兼容标准库的 Marshal
和 Unmarshal
方法。
使用时导入 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
,以及性能对比。
请发表评论