高效生成JSON串——json-gen
source link: https://www.tuicool.com/articles/miq2Y3N
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
概述
游戏服务端的很多操作(包括玩家的和非玩家的)需要传给公司中台收集汇总,根据运营的需求分析数据。中台那边要求传过去的数据为 JSON 格式。一开始我们使用 golang 标准库中的 encoding/json
,发现性能不够理想(因为序列化使用了反射,涉及多次内存分配)。由于数据原始格式都是 map[string]interface{}
,且需要自己一个字段一个字段构造,于是我想可以在构造过程中就计算出最终 JSON 串的长度,那么就只需要一次内存分配了。
使用
下载:
$ go get github.com/darjun/json-gen
导入:
import ( jsongen "github.com/darjun/json-gen" )
使用起来还是比较方便的:
m := jsongen.NewMap() m.PutUint("key1", 123) m.PutInt("key2", -456) m.PutUintArray("key3", []uint64{78, 90}) data := m.Serialize(nil)
data
即为最终序列化完成的 JSON 串。当然,类型可以任意嵌套。代码参见 github
。
github
上有 Benchmark,是标准 JSON 库的性能的 10 倍!
实现
首先定义一个接口 Value
,所有可以序列化为 JSON 的值都实现这个接口:
type Value interface { Serialize(buf []byte) []byte Size() int }
-
Serialize
可以传入一个分配好的内存,该方法会将值序列化后的 JSON 串追加到buf
后面。 -
Size
返回该值最终在 JSON 串中占用的字节数。
分类
我将可序列化为 JSON 串的值分为了 4 类:
-
QuotedValue
:在最终的串中需要用"
包裹起来的值,例如 golang 中的字符串。 -
UnquotedValue
:在最终的串中不需要用"
包裹起来的值,例如uint/int/bool/float32
等。 -
Array
:对应 JSON 中的数组。 -
Map
:对应 JSON 中的映射。
目前这 4 种类型已经可以满足我的需求了,后续扩展也很方便,只需要实现 Value
接口即可。下面根据 Value
的两个接口讨论这 4 种类型的实现。
QuotedValue
底层基于 string
类型定义 QuotedValue
:
type QuotedValue string
由于 QuotedValue
最终在 JSON 串中会有 2 个 "
,故其大小为:长度 + 2。我们来看 Serialize
和 Size
方法的实现:
func (q QuotedValue) Serialize(buf []byte) []byte { buf = append(buf, '"') buf = append(buf, []byte(q)...) return append(buf, '"') } func (q QuotedValue) Size() int { return len(q) + 2 }
UnquotedValue
同样基于 string
类型定义 UnquotedValue
:
type UnquotedValue string
与 QuotedValue
不同的是, UnquotedValue
不需要 "
包裹, Serialize
和 Size
方法的实现可以参见上面,比较简单!
Array
Array
表示一个 JSON 的数组。因为 JSON 数组可以包含任意类型的数据,我们可以基于 []Value
为底层类型定义 Array
:
type Array []Value
这样 Array
在最终 JSON 串中占用的字节包括所有元素大小、元素之间的 ,
和数组前后的 []
, Size
方法实现如下:
func (a Array) Size() int { size := 0 for _, e := range a { // 递归求元素的大小 size += e.Size() } // for [] size += 2 if len(a) > 1 { // for , size += len(a) - 1 } return size }
Serialize
方法递归调用元素的 Serialize
方法,在元素之间添加 ,
,整个数组用 []
包裹。
func (a Array) Serialize(buf []byte) []byte { if len(buf) == 0 { // 如果未传入分配好的空间,根据 Size 分配空间 buf = make([]byte, 0, a.Size()) } buf = append(buf, '[') count := len(a) for i, e := range a { buf = e.Serialize(buf) if i != count-1 { // 除了最后一个元素,每个元素后添加, buf = append(buf, ',') } } return append(buf, ']') }
为了方便操作数组,我给数组添加很多方法,常用的基本类型和 Array/Map
都有对应的操作方法。操作方法命名为 AppendType
和 AppendTypeArray
(其中 Type
为 uint/int/bool/float/Array/Map
等类型名)。
除了 string/Array/Map
,其它的基本类型都使用 strconv
转为字符串,且强制转换为 UnquotedValue
,因为它不需要 "
包裹。
func (a *Array) AppendUint(u uint64) { value := strconv.FormatUint(u, 10) *a = append(*a, UnquotedValue(value)) } func (a *Array) AppendString(value string) { *a = append(*a, QuotedValue(escapeString(value))) } func (a *Array) AppendUintArray(u []uint64) { value := make([]Value, 0, len(u)) for _, v := range u { value = append(value, UnquotedValue(strconv.FormatUint(v, 10))) } *a = append(*a, Array(value)) } func (a *Array) AppendStringArray(s []string) { value := make([]Value, 0, len(s)) for _, v := range s { value = append(value, QuotedValue(escapeString(v))) } *a = append(*a, Array(value)) }
这里有点需要注意,由于 Append*
方法会修改 Array
(即切片),所以接收者需要使用指针!
Map
实现 Map
时,有两种选择。第一种定义为 map[string]Value
,这样结构简单,但是由于 map
遍历的随机性会导致同一个 Map
生成的 JSON 串不一样。最终我选择了第二种方案,即键和值分开存放,这样可以保证在最终的 JSON 串中,键的顺序与插入的顺序相同:
type Map struct { keys []string values []Value }
Map
的大小包含多个部分:
{} " : ,
搞清楚了这些组成部分, Size
方法的实现就简单了:
func (m Map) Size() int { size := 0 for i, key := range m.keys { // +2 for ", +1 for : size += len(key) + 2 + 1 size += m.values[i].Size() } // +2 for {} size += 2 if len(m.keys) > 1 { // for , size += len(m.keys) - 1 } return size }
Serialize
将多个键值对组装:
func (m Map) Serialize(buf []byte) []byte { if len(buf) == 0 { buf = make([]byte, 0, m.Size()) } buf = append(buf, '{') count := len(m.keys) for i, key := range m.keys { buf = append(buf, '"') buf = append(buf, []byte(key)...) buf = append(buf, '"') buf = append(buf, ':') buf = m.values[i].Serialize(buf) if i != count-1 { buf = append(buf, ',') } } return append(buf, '}') }
与 Array
类似,为了方便操作 Map
,我给 Map
添加了很多方法,常见的基本数据类型和 Array/Map
都有对应的操作方法。操作方法命名为 PutType
和 PutTypeArray
(其中 Type
为 uint/int/bool/float/Array/Map
等)。
func (m *Map) put(key string, value Value) { m.keys = append(m.keys, key) m.values = append(m.values, value) } func (m *Map) PutUint(key string, u uint64) { value := strconv.FormatUint(u, 10) m.put(key, UnquotedValue(value)) } func (m *Map) PutUintArray(key string, u []uint64) { value := make([]Value, 0, len(u)) for _, v := range u { value = append(value, UnquotedValue(strconv.FormatUint(v, 10))) } m.put(key, Array(value)) }
结语
我根据自身需求实现了一个生成 JSON 串的库,性能大为提升,尽管还不完善,但是后续扩展也非常简单。希望能给有相同需求的朋友带来启发。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK