go与json

概述

日常开发中经常要在json对象与Go struct之间相互转换。由于Go是一个静态类型的语言,没办法像动态语言那么方便的序列化反序列化json,特别是json的来源不受控的时候。虽然可以使用map[string]interface{}表示动态类型,但是导致非常难看和难以维护的代码。本篇试着使用一些小技巧让序列化json变得更优雅。

动态的json

假定一个场景邮件Envelope,不同的邮件类型的msg字段拥有不一样的结构。比如如下两个邮件json对象的msg字段的结构是不一样的。

1
2
3
4
5
6
7
{
"type": "sound",
"msg": {
"description": "dynamite",
"authority": "the Bruce Dickinson"
}
}
1
2
3
4
5
6
{
"type": "cowbell",
"msg": {
"more": true
}
}

序列化

先定义一些基本类型,使用interface{}来包含不同的数据结构来序列化成不同类型的邮件json

1
2
3
4
5
6
7
8
9
10
11
12
13
type Envelope struct {
Type string `json:"type"`
Msg interface{} `json:"msg"`
}

type Sound struct {
Description string `json:"description"`
Authority string `json:"authority"`
}

type Cowbell struct {
More bool `json:"more"`
}

试着运行一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
s := Envelope{
Type: "sound",
Msg: &Sound{
Description: "dynamite",
Authority: "the Bruce Dickinson",
},
}
buf, err := json.Marshal(s)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// {"type":"sound","msg":{"description":"dynamite","authority":"the Bruce Dickinson"}}

c := Envelope{
Type: "cowbell",
Msg: &Cowbell{
More: true,
},
}
buf, err = json.Marshal(c)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// {"type":"cowbell","msg":{"more":true}}

没有什么特殊的。
完整代码

反序列化

当我们要反序列化jsonEnvelope,那么使用map[string]interface{}来解析msg是很多人喜欢的做法,但是这不是很优雅,而且很容易出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
input := `
{
"type": "sound",
"msg": {
"description": "dynamite",
"authority": "the Bruce Dickinson"
}
}
`
var env Envelope
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}

desc := env.Msg.(map[string]interface{})["description"].(string)
fmt.Println(desc)
// dynamite

使用RawMessage

json.RawMessage是一个有用的类型,可以让你推迟反序列化,其实它只是将原始数据存储为[]byte

1
2
3
4
type Envelope struct {
Type string `json:"type"`
Msg *json.RawMessage `json:"msg"`
}

这样可以明确地控制json的反序列话,并且因为延迟,我们要用到有不同类型分支的数据才去处理。但是序列化的时候要先明确数据结构,如果独立EnvelopeInEnvelopeOut分别用来序列化和反序列化,也不好,其实我们可以结合json.RawMessageinterface{}来使用,不必修改Envelope的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var msg json.RawMessage
env := Envelope{
Msg: &msg,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
switch env.Type {
case "sound":
var s Sound
if err := json.Unmarshal(msg, &s); err != nil {
log.Fatal(err)
}
desc := s.Description
fmt.Println(desc)
// dynamite
default:
log.Fatalf("unknown message type: %q", env.Type)
}

完整代码

拆分和组合

尽管期望json格式比较规范,有分支字段放在一个键下面,但是有时还是会从外部接受到不好的数据格式,比如数据都在顶层。我们可以使用拆分分别使用这些值,也可以如注释组合成一个大的struct来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
input := `
{
"type": "sound",
"description": "dynamite",
"authority": "the Bruce Dickinson"
}
`
var env Envelope
buf := []byte(input)
if err := json.Unmarshal(buf, &env); err != nil {
log.Fatal(err)
}
switch env.Type {
case "sound":
var env Envelope
var s Sound

if err := json.Unmarshal(buf, &struct {
*Envelope
*Sound
}{&env, &s}); err != nil {
log.Fatal(err)
}
// s := struct {
// *Envelope
// *Sound
// }{}
// if err := json.Unmarshal(buf, &s); err != nil {
// log.Fatal(err)
// }
desc := s.Description
fmt.Println(desc)
// dynamite
default:
log.Fatalf("unknown message type: %q", env.Type)
}

完整代码

忽略字段

当要序列化Sound的时候想要忽略Authority字段,可以使用组合的方法覆盖掉原有的参数属性:

1
2
3
4
5
6
type omit *struct{}

type OmitSound struct {
*Sound
Authority omit `json:"authority,omitempty"`
}

这里的技巧是不设置OmitSoundAuthority字段,因为它是一个指针类型,默认值为nil,它将被省略(因为omitempty)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s := &Sound{
Description: "dynamite",
Authority: "the Bruce Dickinson",
}

buf, err := json.Marshal(OmitSound{
Sound: s,
})
// buf, err := json.Marshal(struct {
// *Sound
// Authority bool `json:"authority,omitempty"`
// }{
// Sound: s,
// })
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// {"description":"dynamite"}

也可以像注释中之间使用匿名struct,忽略的字段可以使用*struct{}甚至boolint,使用哪种内置类型无关紧要,只要它具有omitempty标记识别的零值即可。

完整代码

添加额外的字段

添加字段比省略更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s := &Sound{
Description: "dynamite",
Authority: "the Bruce Dickinson",
}

buf, err := json.Marshal(struct {
*Sound
Other string `json:"other"`
}{
Sound: s,
Other: "other",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// {"description":"dynamite","authority":"the Bruce Dickinson","other":"other"}

完整代码

重命名字段

重命名其实是忽略和添加额外字段的组合。请注意,仅当要重命名大struct中的一个或两个字段时,这才是实用的。当重命名所有(大部分)字段时,通常创建一个新struct会比较合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
s := &Sound{
Description: "dynamite",
Authority: "the Bruce Dickinson",
}

buf, err := json.Marshal(struct {
*Sound

// omit keys
OmitAuthority string `json:"authority,omitempty"`

// add keys
Authority string `json:"author"`
}{
Sound: s,
Authority: s.Authority,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// {"description":"dynamite","author":"the Bruce Dickinson"}

完整代码