博客有段时间没更新了,最近刚刚换了工作,入坑了区块链。
最近帮同事解决了一个修改Ethereum项目中struct结构,但是序列化结果不变的问题,其中涉及了go generate和json序列化等知识,今天决定先整理一下json序列化的部分,以后有时间再总结go generate。

我使用的go版本

go version go1.11.1 windows/amd64

一个简单的例子

package main

import (
	"encoding/json"
	"fmt"
	"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       time.Time
}

func main() {
	data := &Data{
		Id:         123,
		Name:       "aaa",
		PrivateKey: "pk",
		Option1:    "op111",
		Option3:    "",
		Time:       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-12T16:42:15.0638139+08:00"}
Unmarshal: {Id:123 Name:aaa PrivateKey: Option1:op111 Option2: Option3: Time:2018-11-12 16:42:15.0638139 +0800 CST}

公有字段

go标准库的json序列化仅支持公有字段,因此字段名的开头字母需要大写。

字段命名

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

忽略字段

使用`json:"-"`指定字段不参与序列化。

忽略字段空值

如果希望某个字段为空值时,不包含在json中,可以使用`json:",omitempty"`来指定,逗号前面可以指定字段名称,例如:`json:"e3,omitempty"`

将数值序列化为字符串

通过 `json:",string"`,可以将数值类型的字段序列化为字符串。

Marshaler和Unmarshaler接口

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

位于:encoding/json/encode.go

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

位于:encoding/json/decode.go

通过Marshaler和Unmarshaler接口,可以自定义序列化和反序列化时需要执行的操作。
下面修改一下上面例子中的日期格式:

package main

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

由于处理的是json字符串,因此dateFormat前后添加了双引号。
这样做,我们定义了一个新的类型MyTime,在使用Time字段时不太方便。

另一种方法:

package main

import (
	"encoding/json"
	"fmt"
	"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       time.Time
}

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

func (d *Data) MarshalJSON() ([]byte, error) {
	type Alias Data
	return json.Marshal(&struct {
		*Alias
		Time string
	}{
		Alias: (*Alias)(d),
		Time:  d.Time.Format(dateFormat),
	})
}

func (d *Data) UnmarshalJSON(b []byte) error {
	type Alias Data
	temp := &struct {
		Time string
		*Alias
	}{
		Alias: (*Alias)(d),
	}
	if err := json.Unmarshal(b, &temp); err != nil {
		return err
	}
	var err error
	if d.Time, err = time.Parse(dateFormat, temp.Time); err != nil {
		return err
	}
	return nil
}

func main() {
	data := &Data{
		Id:         123,
		Name:       "aaa",
		PrivateKey: "pk",
		Option1:    "op111",
		Option3:    "",
		Time:       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日 17:00:31"}
Unmarshal: {Id:123 Name:aaa PrivateKey: Option1:op111 Option2: Option3: Time:2018-11-12 17:00:31 +0000 UTC}

定义一个匿名的struct,增加一个同名的字段,覆盖掉原来的字段。
有个需要注意的地方,对Data需要定义一个别名类型,如果在匿名struct中使用Data结构体,调用Marshal或Unmarshal方法时,会调用MarshalJSON或UnmarshalJSON,导致死递归。

Marshaler和Unmarshaler接口是如何工作的

Marshaler

在调用json.Marshal方法时,会依次调用encoding/json/encode.go中的marshal->reflectValue->valueEncoder->typeEncoder->newTypeEncoder

marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
if t.Implements(marshalerType) {
	return marshalerEncoder
}
if t.Kind() != reflect.Ptr && allowAddr {
	if reflect.PtrTo(t).Implements(marshalerType) {
		return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
	}
}

newTypeEncoder时判断类型,如果实现了Marshaler接口,返回marshalerEncoder。或者可寻址的非指针类型,他的指针实现了Marshaler接口,使用addrMarshalerEncoder。marshalerEncoder和addrMarshalerEncoder方法中都会调用MarshalJSON方法

Unmarshaler

在调用json.Unmarshal方法时,会依次调用encoding/json/decode.go中的unmarshal->value,value方法中会根据类型调用arrayobjectliteralStore方法。
三个方法中分别调用indirect方法获取是否实现了Unmarshaler接口,如果实现了则调用UnmarshalJSON

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
}