高效生成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 串的库,性能大为提高,尽管还不完善,可是后续扩展也很是简单。但愿能给有相同需求的朋友带来启发。

相关文章
相关标签/搜索