概述
遊戲服務端的很多操作(包括玩家的和非玩家的)需要傳給公司中臺收集彙總,根據運營的需求分析資料。中臺那邊要求傳過去的資料為 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 倍!
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 類:
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 串的庫,效能大為提升,儘管還不完善,但是後續擴充套件也非常簡單。希望能給有相同需求的朋友帶來啟發。