游戏服务端的不少操做(包括玩家的和非玩家的)须要传给公司中台收集汇总,根据运营的需求分析数据。中台那边要求传过去的数据为 JSON 格式。一开始咱们使用 golang 标准库中的encoding/json
,发现性能不够理想(由于序列化使用了反射,涉及屡次内存分配)。因为数据原始格式都是map[string]interface{}
,且须要本身一个字段一个字段构造,因而我想能够在构造过程当中就计算出最终 JSON 串的长度,那么就只须要一次内存分配了。git
下载:github
$ go get github.com/darjun/json-gen
导入:golang
import ( jsongen "github.com/darjun/json-gen" )
使用起来仍是比较方便的:json
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 倍!app
Library | Time/op(ns) | B/op | allocs/op |
---|---|---|---|
encoding/json | 22209 | 6673 | 127 |
darjun/json-gen | 3300 | 1152 | 1 |
首先定义一个接口Value
,全部能够序列化为 JSON 的值都实现这个接口:性能
type Value interface { Serialize(buf []byte) []byte Size() int }
Serialize
能够传入一个分配好的内存,该方法会将值序列化后的 JSON 串追加到buf
后面。Size
返回该值最终在 JSON 串中占用的字节数。我将可序列化为 JSON 串的值分为了 4 类:ui
QuotedValue
:在最终的串中须要用"
包裹起来的值,例如 golang 中的字符串。UnquotedValue
:在最终的串中不须要用"
包裹起来的值,例如uint/int/bool/float32
等。Array
:对应 JSON 中的数组。Map
:对应 JSON 中的映射。目前这 4 种类型已经能够知足个人需求了,后续扩展也很方便,只须要实现Value
接口便可。下面根据Value
的两个接口讨论这 4 种类型的实现。指针
QuotedValue
底层基于string
类型定义QuotedValue
:code
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 串的库,性能大为提高,尽管还不完善,可是后续扩展也很是简单。但愿能给有相同需求的朋友带来启发。