遊戲服務端的不少操做(包括玩家的和非玩家的)須要傳給公司中臺收集彙總,根據運營的需求分析數據。中臺那邊要求傳過去的數據爲 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 串的庫,性能大爲提高,儘管還不完善,可是後續擴展也很是簡單。但願能給有相同需求的朋友帶來啓發。