高效生成JSON串——json-gen

概述

遊戲服務端的不少操做(包括玩家的和非玩家的)須要傳給公司中臺收集彙總,根據運營的需求分析數據。中臺那邊要求傳過去的數據爲 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 種類型的實現。spa

QuotedValue

底層基於string類型定義QuotedValue指針

type QuotedValue string
複製代碼

因爲QuotedValue最終在 JSON 串中會有 2 個",故其大小爲:長度 + 2。咱們來看SerializeSize方法的實現:

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不須要"包裹,SerializeSize方法的實現能夠參見上面,比較簡單!

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都有對應的操做方法。操做方法命名爲AppendTypeAppendTypeArray(其中Typeuint/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都有對應的操做方法。操做方法命名爲PutTypePutTypeArray(其中Typeuint/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 串的庫,性能大爲提高,儘管還不完善,可是後續擴展也很是簡單。但願能給有相同需求的朋友帶來啓發。

相關文章
相關標籤/搜索