博客有段时间没更新了,最近刚刚换了工作,入坑了区块链。
最近帮同事解决了一个修改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方法中会根据类型调用array
、object
、literalStore
方法。
三个方法中分别调用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
}