现在的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()将会遵循如下顺序进行查找匹配:

  1. 一个包含Foo标签的字段;
  2. 一个名为Foo的字段;
  3. 一个名为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]anyJson格式,然后调用自己实现的一套解析代码来完成对象绑定。同时加入了自定义验证字段合法性的逻辑。这种思路非常好。

提升性能

标准库提供的方法性能还不够好,有第三方实现更高效的方法可供参考使用。

 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 性能虽好,但看上去貌似很复杂,好像还和平台相关。

我们先不看这种极端优化的库。测试一下standardjson-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

结论:

  1. 通常来说,不管啥库,Encode速度比Decode速度都要快很多,3-5倍是很容易的事。
  2. 不管啥库,Encode速度区别都不大。
  3. Decode数据源小,iterator比标准库快不少,但随着数据源的增大,iterator已无任何优势。

json-iterator意义不大,还是用标准库吧,真要提高性能,再想想别的办法,或者研究一下上面恐怖的sonic

(完)