前言
这两天升级 client-go 版本,涉及 grpc proto 相关代码生成。升级完成后在调试的过程中,发现当数据存入etcd时,一个boolean类型的字段消失了,仔细研究了相关代码半天,最后才发现 go 在 Marshal 结构体时,如果字段是boolean类型,并且设置了omitempty,则会在false的事后作为empty删除。为了避免以后再次栽倒同样的坑里,特意写作本篇博客详细记录golang对json的处理。
golang 标签(tag)系统
在 golang 中,命名都推荐驼峰方式,并且首字母大小写有特殊语法含义:结构体中首字母小写外包无法引用。但是由于经常需要和其他系统进行数据交互,例如转换成 json 格式,存储到 mongodb 等等。这个时候如果用属性名作为键值可能不符合项目要求。
所以 golang 支持 `` 定义标签(tag),在转换成其他格式的时候,会使用其中特定的字段作为键值。例如:
1
2
3
4
5
6
7
8
9
| type User struct {
UserId int `json:"user_id" bson:"user_id"`
UserName string `json:"user_name" bson:"user_name"`
}
u := &User{UserId: 1, UserName: "tony"}
j, _ := json.Marshal(u)
fmt.Println(string(j))
// 输出内容:{"user_id":1,"user_name":"tony"}
|
如果没有定义标签,则输出:
1
| {"UserId":1,"UserName":"tony"}
|
可以看到直接使用了struct的属性名作为键值。
其中还有一个 bson 的声明,这个是用在数据存储到 mongodb 使用的。
struct 成员变量标签 (Tag) 获取
开发者也可以通过 golang 提供的方法获取 Tag 内容,利用 Tag 进行开发。
如何获取呢,可以使用反射包(reflect)中的方法获取:
1
2
3
4
| t := reflect.TypeOf(u)
field := t.Elem().Field(0)
fmt.Println(field.Tag.Get("json"))
fmt.Println(field.Tag.Get("bson"))
|
json package 和 mongodb package 等包就是利用了标签系统完成数据交换。
json 处理流程
Encoding
编码json使用 Marshal 方法
1
| func Marshal(v interface{}) ([]byte, error)
|
Go 结构体 Message
1
2
3
4
5
| type Message struct {
Name string
Body string
Time int64
}
|
实例
1
| m := Message{"Alice", "Hello", 1294706395881547000}
|
我们可以使用 json.Marshal 来获取 JSON-encoded data
1
2
| b, err := json.Marshal(m)
b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
|
只有能够被json正常表示的数据结构才能够完成编码:
- JSON 对象只支持 string 作为 key;因此支持的 Go map 类型必须是 map[string]T(T是json package支持的类型)
- Channel,complex,和 function 类型不支持编码
- Pointer 会按照指向的值进行编码(如果pointer时nil,则编码成‘null’)
json package 只编码 exported filed(也就是大写字母开头的字段)。因此只有导出的字段出现在JSON的输出中。
Decoding
解析 JSON 数据使用 Unmarshal 方法:
1
| func Unmarshal(data []byte, v interface{}) error
|
我们需要创建一个变量存放解析后的数据
调用 Unmarshal
1
| err := json.Unmarshal(b, &m)
|
如果 b 包含有效的 JSON 数据,并且对应 m 的结构,方法返回 err 为 nil, 并且 b 中的数据解析后放入 m。
1
2
3
4
5
| m = Message{
Name: "Alice",
Body: "Hello",
Time: 1294706395881547000,
}
|
Unmarshal 如何识别哪个字段存储解析的数据?如果解析获得一个JSON key “Foo”,Unmarshall
会在目标结构体中找合适的字段(以下条件按顺序查找):
- 导出的字段,定义了 tag “Foo”(Go Spec)
- 导出的字段,名字是 “Foo”
- 导出的字段,名字是 “FoO” 或 “FOO” 获取其他在大小写不敏感情况下与 “Foo” 匹配
当 Json 数据中的结构和 Go type 不匹配时会发生什么?
1
2
3
| b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)
|
Unmarshal
会解析和目标结构体匹配的字段,忽略不认识的字段。这个特性在当你希望只获取大量json数据中的部分字段时十分有用,这也表示任何任意非导出的字段(小写字母开头的字段)不会被 Unmarshal
影响。
使用 Tag
一般在 golang 使用 json package,都会使用标签系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Field appears in JSON as key "myName".
Field int `json:"myName"`
// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`
// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`
// Field is ignored by this package.
Field int `json:"-"`
// Field appears in JSON as key "-".
Field int `json:"-,"`
// Field appears in JSON as string type.
Field int `json:,string,omitempty`
Field int `json:,omitempty,string`
|
通过标签系统可以对转换后的 JSON 数据进行自定义:
json:"myName"
定义 JSON key 为 myNamejson:"myName,omitempty"
定义 JSON key 为 myName,并且当 empty 的时候删除该字段(empty的定义为:0,false和nil 指针,nil interface和空的slice、map、array和string)json:"-"
在 JSON 中忽略该字段json:"-,"
在 JSON 中输出 key 值为 “-”json:,string,omitempty
和 json:,omitempty,string
定义该字段在 JSON 中按 string 类型存储,并且 omitempty。(只有 string、float、integer 和 boolean 字段支持这么定义)
高级用法
使用 interface{} 解析未知 JSON
json pacakge 使用 map[string]interface 和 []interface{} 存储各种 JSON 对象和数组。任意有效的JSON数据类型都能轻松存储到 interface{} 中,默认的具体 Go types 是:
- bool 对应 JSON boolean
- float64 对应 JSON numbers(精度上会有损失)
- string 对应 JSON strings
- nil 对应 JSON null
一个例子:
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
| b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
var f interface{}
err := json.Unmarshal(b, &f)
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
m := f.(map[string]interface{})
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")
}
}
|
Unmarshal 为引用类型申请内存
我们定义一个类型包含引用
1
2
3
4
5
6
7
8
| type FamilyMember struct {
Name string
Age int
Parents []string
}
var m FamilyMember
err := json.Unmarshal(b, &m)
|
Unmarshalling JSON object 到 FamilyMember 里,相比之前例子, Parents 是引用类型,默认值是nil。如果 JSON object有值写入Parents,Unmarshal
会为 Parents 申请内存。这个是 Unmarshal 对于引用类型(pointers,slicemaps)的典型工作场景。
1
2
3
| type Foo struct {
Bar *Bar
}
|
如果 JSON object 中包含 Bar 字段,Unmarshal 会为 Bar 申请内存,反之则保持 Bar 为 nil。
支持 Streaming
json package 支持对 Stream 进行 Decoder 和 Encoder。使用 NewDecoder 和 NewEncoder 装饰 io.Reader 和 io.Writer inteface typs.
1
2
| func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encodergo
|
一个例子:
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
| 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)
}
}
}
|
对于更普遍的Reader和Writer,Encoder和Decoder类型可以使用在更广泛的场景中。例如对HTTP连接,WebSockets和文件进行读写。
需要注意的问题
omitempty
omitempty 中的 empty 并不仅仅指 empty, 还包括 0,false和nil 指针,nil interface和空的slice、map、array和string。 定义了该 tag 后,boolean、integer、float32、string等属性为默认值时,输出的 JSON 数据就会移除该属性。
在一些需要区分未初始化和0值的场景下,omitempty的使用会造成混淆,这个时候可以使用指针来作为属性。未初始化状态下指针为 nil,输出 JSON 时会被 omit;初始化后 0 值和非 0 值都会正常输出,这样属性的三个状态:未初始化、0值和非0值就可以正常区分了。
JSON反序列化成interface{}对Number的处理
JSON 规范中,对于数字类型,并不区分整型还是浮点型。
对于如下 JSON 文本:
1
2
3
4
| {
"name": "ethancai",
"fansCount": 9223372036854775807
}
|
如果反序列化不指定结构体类型或者变量类型,则JSON中的数字类型,默认被反序列化成 float64
类型
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
| package main
import (
"encoding/json"
"fmt"
"reflect"
)
func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`
var user interface{} // 不指定反序列化的类型
err := json.Unmarshal([]byte(jsonStream), &user)
if err != nil {
fmt.Println("error:", err)
}
m := user.(map[string]interface{})
fansCount := m["fansCount"]
fmt.Printf("%+v \n", reflect.TypeOf(fansCount).Name())
fmt.Printf("%+v \n", fansCount.(float64))
}
// Output:
// float64
// 9.223372036854776e+18
|
如果 fansCount
精度比较高,反序列化成 float64
类型的数值时存在丢失精度的问题。
如何解决这个问题,看下面的程序:
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
37
38
| package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
func main() {
const jsonStream = `
{"name":"ethancai", "fansCount": 9223372036854775807}
`
decoder := json.NewDecoder(strings.NewReader(jsonStream))
decoder.UseNumber() // UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
var user interface{}
if err := decoder.Decode(&user); err != nil {
fmt.Println("error:", err)
return
}
m := user.(map[string]interface{})
fansCount := m["fansCount"]
fmt.Printf("%+v \n", reflect.TypeOf(fansCount).PkgPath() + "." + reflect.TypeOf(fansCount).Name())
v, err := fansCount.(json.Number).Int64()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%+v \n", v)
}
// Output:
// encoding/json.Number
// 9223372036854775807
|
使用 json.NewDecoder 可以将 JSON 数字转换成 json.Number。json.Number 内部实现机制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
|
其实就是延迟处理,保存了 JSON 数字的原始字符串,使用的时候根据需要进行转换。
总结
其实 Golang 对于 json 的支持已经十分完善,通过和标签系统配合,序列化和反序列化 json 字符串只要几行代码,只是在使用过程中要清楚 go 提供的默认机制,避免踩坑。
参考