现在的Web应用,几乎都是采用JSON格式进行数据交互。
Go语言内建对JSON的支持。使用Go语言内置的encoding/json标准库,开发者可以轻松使用Go程序生成和解析JSON格式的数据。在Go语言实现JSON的编码和解码时,遵循RFC4627协议标准。深入理解Go是如何编码解码JSON格式数据非常有必要。
JSON规范
JSON规范,参考:
www.runoob.com/json/json-syntax.html
特别说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// ++++++++++++++++++++++++++++++++++++++需要转义的字符
// \\ 反斜杠
// \" 双引号
// \' 单引号
// \/ 正斜杠
// \b 退格符
// \f 换页符
// \t 制表符
// \n 换行符
// \r 回车符
// \u 后面跟十六进制字符 (比如笑脸表情 \u263A)
// +++++++++++++++++++++++++++++++++++++++++++++++++++
// shitespace(只有下面四种)
// c == ' ' || c == '\n' || c == '\r' || c == '\t'
|
编码成JSON字符串
这个比较简单,就是将对象转换成JSON格式字符串。内置方法如下:
1
2
3
4
5
6
|
json.Marshal // 正常编码
json.MarshalIndent // 编码并带格式化输出,比如带上缩进,可读性更友好
// 编码对象,并指定写入流
enc := json.NewEncoder(writer)
enc.Encode(object)
|
解码JSON字符串
解码和编码对应,但是解码过程相对复杂的多;其也有几个对应的方法:
1
2
3
4
5
6
7
8
|
var jsonStr = `{"name": "Gopher","title": "Programmer"}`
var user DemoUser
// 可以读取一个输入流,然后解码到对象
dec := json.NewDecoder(strings.NewReader(jsonStr))
_ = dec.Decode(&user)
// 或将字符切片解码到对象
_ = json.Unmarshal([]byte(jsonStr), &user)
|
注意:解码后的目标对象通常是一个struct,或者是map[string]any(也就是KV)。
上面两个解码函数,底层调用的解析方法是相同的,只是针对不同数据源做了封装而已。
json.Unmarshal()
函数会根据一个约定的顺序查找目标结构中的字段,如果找到一个即发生匹配。假设一个JSON对象有个名为"Foo"的索引,要将"Foo"所对应的值填充到目标结构体的目标字段上,json.Unmarshal()将会遵循如下顺序进行查找匹配:
- 一个包含Foo标签的字段;
- 一个名为Foo的字段;
- 一个名为Foo或者除了首字母其他字母不区分大小的命名为Foo的字段。
注意:这些字段在类型声明中必须都是以大写字母开头、可被导出的字段。如果JSON中的字段在Go目标类型中不存在,json.Unmarshal()函数在解码过程中会丢弃该字段。
解码过程中有几个重要的方法,简要说明如下:
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
|
// 分三种不同情况:数组,对象,key-value。递归解析
func (d *decodeState) value(v reflect.Value) error {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
d.array(v)
case scanBeginObject:
d.object(v)
case scanBeginLiteral:
d.literalStore(d.data[start:d.readIndex()], v, false)
}
}
// 如果解析目标是struct,而不是一个map,会将反射解析出的StructFields做缓存处理,提高性能
// 注意:无论编码还是解码Struct,反射解析的结果都会缓存在内存中而且不会主动释放
func typeFields(t reflect.Type) structFields
// 查看源代码会发现字段名的确定顺序:
// 1. 取tag为json的配置, 如果配置值“-”那么这个字段直接跳过不解析
// 2. 直接取Struct可导出字段的字段名
// 支持目标结构体嵌套,比如:
type DemoUser struct {
Name string `json:"name"`
Title string `json:"title"`
Contact struct {
Home string `json:"home"`
Cell string `json:"cell"`
} `json:"contact"`
}
|
常见WEB框架对象绑定
JSON字符串解码(也就是反序列化)在Web开发中经常见到,常见框架是如何处理的呢?
Gin
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
|
func (c *Context) ShouldBind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
|
Gin针对各种不同的数据源分别给了绑定的API,让框架更通用。还可以通过请求的ContentType类型,匹配相应格式的解码方法。总体来说JSON或者XML格式的数据,需要调用标准库来绑定;其它形式提交的数据都要转换成type Values map[string][]string
之后,调用自己实现的form_mapping.go
逻辑来完成对象绑定。
Echo
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
|
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
req := c.Request()
names := c.ParamNames()
values := c.ParamValues()
params := map[string][]string{}
for i, name := range names {
params[name] = []string{values[i]}
}
if err := b.bindData(i, params, "param"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
if req.ContentLength == 0 {
return
}
ctype := req.Header.Get(HeaderContentType)
switch {
case strings.HasPrefix(ctype, MIMEApplicationJSON):
json.NewDecoder(req.Body).Decode(i)
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
xml.NewDecoder(req.Body).Decode(i)
case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
params, err := c.FormParams()
b.bindData(i, params, "form")
default:
return ErrUnsupportedMediaType
}
return
}
|
Echo没有像Gin提供不同的数据源的绑定方法,Web提交数据的对象绑定只有一个Bind
搞定,这很简洁。至于绑定的算法和Gin如出一辙,JSON和XML格式的数据都需要标准库来处理,其它比如UrlParams、Form、Query等数据都按照type Values map[string][]string
类型自己实现的解析逻辑。
Echo还有一个特点,就是在Bind过程中,依次检查所有可能的数据源,全部加载到指定对象上。
go-zero
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package httpx
// Parse parses the request.
func Parse(r *http.Request, v interface{}) error {
if err := ParsePath(r, v); err != nil {
return err
}
if err := ParseForm(r, v); err != nil {
return err
}
if err := ParseHeaders(r, v); err != nil {
return err
}
return ParseJsonBody(r, v)
}
// ParseHeaders parses the headers request.
func ParseHeaders(r *http.Request, v interface{}) error {
return encoding.ParseHeaders(r.Header, v)
}
// ParseForm parses the form request.
func ParseForm(r *http.Request, v interface{}) error {
params, err := GetFormValues(r)
if err != nil {
return err
}
return formUnmarshaler.Unmarshal(params, v)
}
// ParseJsonBody parses the post request which contains json in body.
func ParseJsonBody(r *http.Request, v interface{}) error {
if withJsonBody(r) {
reader := io.LimitReader(r.Body, maxBodyLen)
return mapping.UnmarshalJsonReader(reader, v)
}
return mapping.UnmarshalJsonMap(nil, v)
}
// ParsePath parses the symbols reside in url path.
// Like http://localhost/bag/:name
func ParsePath(r *http.Request, v interface{}) error {
vars := pathvar.Vars(r)
m := make(map[string]interface{}, len(vars))
for k, v := range vars {
m[k] = v
}
return pathUnmarshaler.Unmarshal(m, v)
}
|
这个框架和常见的不一样。他最终都是将不同的数据源转换成map[string]any
的Json
格式,然后调用自己实现的一套解析代码来完成对象绑定。同时加入了自定义验证字段合法性的逻辑。这种思路非常好。
提升性能
标准库提供的方法性能还不够好,有第三方实现更高效的方法可供参考使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// github.com/json-iterator/go
// 实践中可以这样自定义
package myjson
import (
jsonIterator "github.com/json-iterator/go"
)
var (
jit = jsonIterator.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
UseNumber: true,
}.Froze()
Marshal = jit.Marshal
MarshalIndent = jit.MarshalIndent
NewDecoder = jit.NewDecoder
NewEncoder = jit.NewEncoder
)
|
关于性能的问题,可以参考阅读:
https://zhuanlan.zhihu.com/p/461772555
https://zhuanlan.zhihu.com/p/586050976
https://juejin.cn/post/7139082554304364581
上面文章中提到的字节开源项目 bytedance/sonic 性能虽好,但看上去貌似很复杂,好像还和平台相关。
我们先不看这种极端优化的库。测试一下standard
和json-iterator
的性能差异。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
import "encoding/json"
import jsonIterator "github.com/json-iterator/go"
type KV map[string]any
func DecodeStdJson(jsonStr []byte) {
obj := make(KV, 0)
_ = json.Unmarshal(jsonStr, &obj)
}
func DecodeIterJson(jsonStr []byte) {
obj := make(KV, 0)
_ = jsonIterator.Unmarshal(jsonStr, &obj)
}
func EncodeStdJson(obj any) {
json.Marshal(obj)
}
func EncodeIterJson(obj any) {
jsonIterator.Marshal(obj)
}
// +++++ test
var jsonBytes []byte
var itemObj *Item
var mini = `{"msg":"还没有登录","_stamp":1678350236848,"status":"fai","msg_code":0,`+
`"Inner":{"status":"fai","msg_code":0}}`
func init() {
jsonBytes = []byte(mini)
itemObj = &Item{
Msg: "还没有登录", Stamp: 1678350236848, Status: "fai", MsgCode: 0,
Inner: Inner{Status: "fai", MsgCode: 0},
}
}
type Item struct {
Msg string
Stamp int64
Status string
MsgCode int64
Inner Inner
}
type Inner struct {
Status string
MsgCode int64
}
func BenchmarkStdJson(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
DecodeStdJson(jsonBytes)
}
}
func BenchmarkIterJson(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
DecodeIterJson(jsonBytes)
}
}
func BenchmarkEncodeStdJson(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
EncodeStdJson(itemObj)
}
}
func BenchmarkEncodeIterJson(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
EncodeIterJson(itemObj)
}
}
|
改变数据源长度测试结果会不一样,下面是数据源大概100B时候的测试结果:
jsonx> go test -bench=Json$ -benchmem
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkStdJson-16 436838 2746 ns/op 1216 B/op 34 allocs/op
BenchmarkIterJson-16 962016 1287 ns/op 1016 B/op 31 allocs/op
BenchmarkEncodeStdJson-16 3677107 326.2 ns/op 112 B/op 1 allocs/op
BenchmarkEncodeIterJson-16 3260479 363.4 ns/op 120 B/op 2 allocs/op
PASS
ok 5.751s
结论:
- 通常来说,不管啥库,Encode速度比Decode速度都要快很多,3-5倍是很容易的事。
- 不管啥库,Encode速度区别都不大。
- Decode数据源小,
iterator
比标准库快不少,但随着数据源的增大,iterator
已无任何优势。
用json-iterator
意义不大,还是用标准库吧,真要提高性能,再想想别的办法,或者研究一下上面恐怖的sonic
。
(完)