GO-序列化之JSON

Easy to Go

Posted by Michelle on May 29, 2022

JSON

JSON是一种简单的数据交换格式,它最常用于在浏览器中运行的Web后端和JavaScript程序之间的通信,是日常开发中使用最多的数据交换格式之一。 几乎所有现代编程语言都会对JSON的序列化方法内置于标准库中,Go也不例外。

GO语言中的JSON

Go语言通过encoding/json 对外提供标准的 JSON 序列化和反序列化方法,其中最常用的是 encoding/json.Marshal 和 encoding/json.Unmarshal。

JSON 反序列化的开销是序列化开销的好几倍,这是由于Go 语言中的 JSON 序列化过程不需要被序列化的对象预先实现任何接口,它会通过反射获取结构体或者数组中的值并以树形的结构递归地进行编码,标准库也会根据 encoding/json.Unmarshal 中传入的值对 JSON 进行解码。

同时Go 语言 JSON 标准库编码和解码的过程大量地运用了反射这一特性。 我们先来简单介绍一下 JSON 标准库中的接口和标签,这两个特性可以让我们实现编码/解码的定制化。

标签

在默认情况下,当我们在序列化和反序列化结构体时,标准库都会认为字段名和 JSON 中的键具有一一对应的关系,所以我们会使用标签这一特性,直接建立键与字段之间的映射关系。

type Author struct {
    Name string `json:"name,omitempty"`
    Age  int32  `json:"age,string,omitempty"`
    Salary int32 `json:"-"`
}

1.字段命名:默认json中的key与字段名一致,可以通过json:"key名称" 来指定。

2.string标签:表示当前的整数或者浮点数是由 JSON 中的字符串表示的。

3.omitempty标签:在字段为空值时,直接在生成的 JSON 中忽略对应的键值对,例如:”age”: 0、”author”: “” 等。

4.忽略字段:使用json:"-"指定字段不参与序列化。

标准库会使用如下所示的 encoding/json.parseTag 来解析标签,我们可以看出, 标签名和标签选项都以逗号连接,最前面的字符串为标签名,后面的都是标签选项

func parseTag(tag string) (string, tagOptions) {
	if idx := strings.Index(tag, ","); idx != -1 {
		return tag[:idx], tagOptions(tag[idx+1:])
	}
	return tag, tagOptions("")
}

接口

Marshaler和Unmarshaler接口

在 JSON 序列化和反序列化的过程中,它会使用反射判断结构体类型是否实现了上述接口,如果实现了上述接口就会优先使用对应的方法进行编码和解码操作。

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

下面是一个示例,我们通过实现Marshaler和Unmarshaler对时间进行自定义的编码/解码。

package json_test

import (
	"encoding/json"
	"fmt"
	"time"
)

type MyTime time.Time

type Data struct {
	Id         int    `json:",string"`
	Name       string `json:"name"`
	PrivateKey string `json:"-"`
	Option1    string `json:",omitempty"`
	Option2    string `json:",omitempty"`
	Option3    string `json:"op3,omitempty"`
	Time       MyTime
}

var dateFormat = `"` + "2006年01月02日 15:04:05" + `"`

func (t MyTime) MarshalJSON() ([]byte, error) {
	return []byte(time.Time(t).Format(dateFormat)), nil
}

func (t *MyTime) UnmarshalJSON(b []byte) error {
	temp, err := time.Parse(dateFormat, string(b))
	if err != nil {
		return err
	}
	*t = MyTime(temp)
	return nil
}

func main() {
	data := &Data{
		Id:         123,
		Name:       "aaa",
		PrivateKey: "pk",
		Option1:    "op111",
		Option3:    "",
		Time:       MyTime(time.Now()),
	}
	b, err := json.Marshal(data)
	if err != nil {
		panic(err)
	}
	fmt.Println("Marshal: ", string(b))
	var newData Data
	if err := json.Unmarshal(b, &newData); err != nil {
		panic(err)
	}
	fmt.Printf("Unmarshal: %+v\n", newData)
}

输出:

Marshal:  {"Id":"123","name":"aaa","Option1":"op111","Time":"2018年11月12日 16:52:59"}
Unmarshal: {Id:123 Name:aaa PrivateKey: Option1:op111 Option2: Option3: Time:{wall:0 ext:63677638379 loc:<nil>}}

TextMarshaler和TextUnmarshaler接口

TextMarshaler与Marshaler类似,优先级比Marshaler低。返回的是文本内容,不需要自己添加双引号。 上面例子中MyTime的MarshalJSON方法可以使用MarshalText代替:

func (t MyTime) MarshalText() ([]byte, error) {
  return []byte(time.Time(t).Format("2006年01月02日 15:04:05")), nil
}

总的来说,我们可以在任意类型上实现上述这四个方法自定义最终的结果,后面的两个方法的适用范围更广,但是不会被 JSON 标准库优先调用。

序列化

我们通过Marshal方法序列化JSON:

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

我们通过一个简单示例了解一下使用方法:

// 定义一个Message结构体
type Message struct {
    Name string
    Body string
    Time int64
}

初始化一个Message:

message := Message{"EasyToGo", "Hello World", 1294706395881547000}
result, err := json.Marshal(m)

如果一切运转正常,err会为null;result会是含有JSON数据的[]byte

result== []byte(`{"Name":"EasyToGo","Body":"Hello World","Time":1294706395881547000}`)

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

1.只有 大写字母开头的字段 才会被 JSON 编码/解码如上段代码中的Name、Body、Time。

2.无法对通道,复杂类型和函数类型进行编码。

3.不支持循环数据结构;它们会导致 Marshal 函数陷入无限循环。

4.JSON 对象仅支持将字符串作为键;要编码 Go 集合类型,它必须采用 map[string]T 的形式 (其中 T 是 JSON 程序包支持的任何 Go 类型)。

#反序列化 为了解码 JSON 数据,我们使用 Unmarshal 函数

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

与执行过程确定的序列化相比,反序列化的过程是逐渐探索的过程,所以会复杂很多,开销也会高出几倍。因为反序列化的使用相对比较繁琐,所以需要传入一个变量帮助标准库进行反序列化:

//创建存储解码数据的变量
var message Message
err := json.Unmarshal(result, &message)

若成功调用

message = Message{
    Name: "EasyToGo",
    Body: "Hello World",
    Time: 1294706395881547000,
}

如果JSON 数据的结构与 Go 类型不完全匹配时,Unmarshal 函数将仅解码在目标类型中可以找到的字段:

b := []byte(`{"Name":"Michelle","Age":"20"}`)
var message Message
err := json.Unmarshal(result, &message)

上面代码中,Unmarshal函数只会解码Name字段,age字段将被忽略。

解码任意数据

接下来我们讨论一种常见情况:事先并不知道JSON中的数据格式和字段该怎么办?例如下面这段代码,假定它是一种未知结构,我们应该怎么进行编码/解码呢?

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

我们可以使用interface{}来进行解码,interface{}为空接口,go的每一种类型都实现了该接口,因此,任何其他类型的数据都可以赋值给interface{}类型。

var f interface{}
// 使用interface{}来进行解码
err := json.Unmarshal(b, &f)
m := f.(map[string]interface{})
// 使用 range 语句遍历集合,根据类型的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")
    }
}

小结

1.JSON是一种树形的数据结构,无论是序列化还是反序列化,都会遵循自顶向下的编码和解码过程,使用递归的方式处理 JSON 对象。

2.作为标准库的 JSON 提供的接口非常简洁,虽然它的性能相比于其他数据格式不算优秀,但是作为框架它提供了很好的通用性。