还在用 map[string]interface{} 处理 JSON?告诉你一个更高效的方法——jsonvalue

本文介绍的是 jsonvalue 库,这是我个人在 Github 上开发的第一个功能比较多而全的 Go 库。目前主要是在腾讯未来社区的开发中使用,用于取代 map[string]interface{}


为什么开发这个库?

Go 是后台开发的新锐。Go 工程师们早期就会接触到 "encoding/json" 库:对于已知格式的 JSON 数据,Go 的典型方法是定义一个 struct 来序列化和反序列化 (marshal/unmarshal)。

但是对于未知格式,亦或者是不方便固定格式的情形,典型的解决方法是采用 map[string]interface{} 来处理。但是在实际应用中,这个方案是存在一些不足的。


map[string]interface{} 存在的不足

有一些情况下,我们确实需要采用 map[string]interface{} 来解析并处理 JSON,这往往出现在中间件、网关、代理服务器等等需要处理全部或部分格式未知的 JSON 逻辑中。

判断值类型时不方便

假设我有一个 unmarshal 之后的 map: m := map[string]interface{}{},当我要判断一个键值对(如 "aNum")是不是数字时,需要分别判断两种情况:

v, exist := m["aNum"]
if false == exist {
    return errors.New("aNum does not exist")
}

n, ok := v.(float64)
if false == ok {
    return fmt.Errorf("'%v' is not a number", v)
}

获取较深的字段时不方便

比如腾讯云 API,其数据返回格式嵌套几层,示意如下:

{
    "Response": {
        "Result": {
            "//": "这里我假设需要查找下面这个字段:",
            "AnArray": [
                {
                    "SomeString": "Hello, world!"
                }
            ]
        }
    }
}

当接口出错的时候,会返回:

{
    "Response": {
        "Error": {
            "Code": "error code",
            "Message": "error message"
        }
    }
}

假设在正常逻辑中,我们因为一些因素,必须使用 map[string]interface{} 来解析数据。难么当需要判断 Response.Result.AnArray[0].SomeString 的值时,由于我们不能100%信任对端的数据(可能服务器被劫持了、崩溃了、被入侵了等等可能),而需要对各个字段进行检查,因而完整的代码如下:

    m := map[string]interface{}{}
    // 一些 unmarshal 动作 
    // ......
    //
    // 首先要判断接口是否错误
    var response map[string]interface{}
    var ok bool
    //
    // 首先要获取 Response 信息
    if v, exist := m["Response"]; !exist {
        return errors.New("missing Response")
    //
    // 然后需要判断 Response 是不是一个 object 类型
    } else if response, ok = v.(map[string]interface{}); !ok {
        return errors.New("Response is not an object")
    //
    // 然后需要判断是否有 Error 字段
    } else if e, exist = response["Error"]; exist {
        return fmt.Errorf("API returns error: %_+v", e)
    }
    //
    // 然后才判断具体的值
    // 首先,还需要判断是否有 Result 字段
    if resultV, exist := response["Result"]; !exist {
        return errors.New("missing Response.Result")
    //
    // 然后再判断 Result 字段是否 object
    } else if result, ok := resultV.(map[string]interface{}); !ok {
        return errors.New("Response.Result is not an object")
    //
    // 然后再获取 AnArray 字段
    } else if arrV, exist := resule["AnArray"]; !exist {
        return errors.New("missing Response.Result.AnArray")
    //
    // 然后再判断 AnArray 的类型
    } else if arr, ok := arrV.([]interface{}); !ok {
        return errors.New("Response.Result.AnArray is not an array")
    // 然后再判断 AnArray 的长度
    } else if len(arr) < 1 {
        return errors.New("Response.Result.AnArray is empty")
    //
    // 然后再获取 array 的第一个成员,并且判断是否为 object
    } else if firstObj, ok := arr[0].(map[string]interface{}); !ok {
        return errors.New("Response.Result.AnArray[0] is not an object")
    //
    // 然后再获取 SomeString 字段
    } else if v, exist := firstObj["SomeString"]; !exist {
        return errors.New("missing Response.Result.AnArray[0].SomeString")
    //
    // 然后再判断 SomeString 的类型
    } else if str, ok := v.(string); !ok {
        return errors.New("Response.Result.AnArray[0].SomeString is not a string")
    //
    // 终于完成了!!!
    } else {
        fmt.Printf("SomeString = '%s'\n", str)
        return nil
    }

不知道读者是什么感觉,反正我是要掀桌了……

不知道读者是什么感觉,反正我是要掀桌了……

Marshal() 效率较低

Unmarshal() 中,map[string]interface{} 类型的反序列化效率比 struct 略低一点,但大致相当。但在 Marshal() 的时候,两者的差别就非常明显了。根据后文的一个测试方案,map 的耗时是 struct 的五倍左右。一个序列化/反序列化操作下来,就要多耗费一倍的时间。


jsonvalue 功能介绍

Jsonvalue 是一个用于处理 JSON 的 Go 语言库。其中解析 json 文本的部分基于 jsonparser 实现。而解析具体内容、JSON 的 CURD、序列化工作则独立实现。

首先我们介绍一下基本的使用方法

反序列化

Jsonvalue 也提供了响应的 marshal/unmarshal 接口来序列化/反序列化 JSON 串。我们以前面获取 Response.Result.AnArray[0].SomeString 的功能举例说明,包含完整错误检查的代码如下:

    // 反序列化
    j, err := jsonvalue.Unmarshal(plainText)
    if err != nil {
        return err
    }

    // 判断接口是否返回了错误
    if e, _ := jsonvalue.Get("Response", "Error"); e != nil {
        return fmt.Errorf("Got error from server: %v", e)
    }

    // 获取我们要的字符串
    str, err := j.GetString("Response", "Result", "AnArray", 0, "SomeString")
    if err != nil {
        return err
    }
    fmt.Printf("SomeString = '%s'\n", str)
    return nil

结束了。是不是很简单?在 j.GetString(...) 中,函数完成了以下几个功能:

  1. 允许传入不定数的参数,依次往下解析
  2. 解析到某一层时,如果当前参数类型为 string,则自动判断当前层级是否为 Json object,如果不是,则返回 error
  3. 解析道某一层时,如果当前参数类型为整型数字,则自动判断当前层级是否为 Json array,如果不是,则返回 error
  4. 从 array 中取值时,如果给定的数组下标超出 array 长度,则返回 error
  5. 从 object 中取值时,如果制定的 key 不存在,则返回 error
  6. 最终获取到制定的键值对,则会判断一下类型是否为 Json string,是的话返回 string 值,否则返回 error

也就是说,在前面的问题中一长串的检查,都在这个函数中自动帮你解决了。
除了 string 类型外,jsonvalue 也支持 GetBool, GetNull, GetInt, GetUint, GetInt64, GetArray, GetObject 等等一系列的类型获取,只要你想到的 Json 类型都提供。

JSON 编辑

大部分情况下,我们需要编辑一个 JSON object。使用 j := jsonvalue.NewObject()。后续可以采用 SetXxx().At() 系列函数设置子成员。与前面所说的 GetXxx 系列函数一样,其实 jsonvalue 也支持一站式的复杂结构生成。下面我们一个一个说明:

设置 JSON object 的子成员

比如在 j 下设置一个 string 类型的子成员:someString = 'Hello, world!'

j.SetString("Hello, world!").At("someString")   // 表示 “在 'someString' 键设置 string 类型值 'Hello, world!'”

同样地,我们也可以设置其他的类型:

j.SetBool(true).At("someBool") // "someBool": true
j.SetArray().At("anArray")     // "anArray": []
j.SetInt(12345).At("anInt")    // "anInt": 12345

设置 JSON array 的子成员

为 JSON 数组添加子成员也是必要的功能。同样地,我们先创建一个数组:a := jsonvalue.NewArray()。对数组的基本操作有以下几个:

// 在数组的开头添加元素
a.AppendString("Hello, world!").InTheBegging()

// 在数组的末尾添加元素
a.AppendInt(5678).InTheEnd()

// 在数组中指定位置的前面插入元素
a.InsertFloat32(3.14159).Before(1)

// 在数组中指定位置的后面插入元素
a.InsertNull().After(2)

快速编辑 JSON 更深层级的内容

针对编辑场景,jsonvalue 也提供了快速创建层级的功能。比如我们前文提到的 JSON:

{
    "Response": {
        "Result": {
            "AnArray": [
                {
                    "SomeString": "Hello, world!"
                }
            ]
        }
    }
}

使用 jsonvalue 只需要两行就可以生成一个 jsonvalue 类型对象(*jsonvalue.V):

j := jsonvalue.NewObject()
j.SetString("Hello, world!").At("Response", "Result", "AnArray", 0, "SomeString")

At() 函数中,jsonvalue 会递归地检查当前层级的 JSON 值,并且按照参数的要求,如有必要,自动地创建相应的 JSON 值。具体如下:

  1. 允许传入不定数的参数,依次往下解析
  2. 解析到某一层时,如果下一层参数类型为 string,则自动判断当前层级是否为 Json object,如果不是,则返回 error
  3. 解析道某一层时,如果下一层参数类型为整型数字,则自动判断当前层级是否为 Json array,如果不是,则返回 error
  4. 解析到某一层时,如果没有后续参数了,那么这就是最终目标,则按照前面的 SetXxxx 所指定的子成员类型,创建子成员

具体到上面的例子,那么整个操作逻辑如下:

  1. SetString() 函数表示准备设置一个 string 类型的子成员
  2. At() 函数表示开始在 JSON 对象中寻址。
  3. "Response" 参数,首先检查到这不是最后一个参数,那么首先判断当前的 j 是不是一个 object 对象,如果不是,则返回 error
  4. 如果 "Response" 对象存在,则取出;如不存在,则创建,然后内部递归地调用 response.SetString("Hello, world!").At("Result", "AnArray", 0, "SomeString")
  5. "Result" 同理
  6. 拿到 "Result" 层的对象之后,检查下一个参数,发现是整型,则函数判断为预期下一层目标 "AnArray" 应该是一个数组。那么函数内首先获取这个目标,如果不存在,则创建一个数组;如果存在,则如果该目标不是数组的话,会返回 error
  7. 拿到 "AnArray" 之后,当前参数为整数。这里的逻辑比较复杂:

    1. 如果该参数等于 -1,则表示在当前数组的末尾添加元素
    2. 如果该参数的值等于当前数组的长度,也表示在当前数组的末尾添加元素
    3. 如果该参数的值大于等于零,且小于当前数组的长度,则表示将当前数组的指定位置替换为新的指定元素
  8. 最后一个参数 "SomeString" 是一个 string 类型,那么表示 AnArray[0] 应是一个 object,则在 AnArray[0] 位置创建一个 JSON object,并且设置 {"SomeString":"Hello, world!"}

其实可以看到,上面的流程对于目标为数组类型来说,不太直观。因此对于目标 JSON 为数组的层级,前文提到的 AppendInsert 函数也支持不定量参数。举个例子,如果我们需要在上述提及的 Response.Result.AnArray 数组末尾添加一个 true 的话,可以这么调用:

j.AppendBool(true).InTheEnd("Response", "Result", "AnArray")

序列化

将一个 jsonvalue.V 序列化的方式也很简单:b, _ := j.Marshal() 即可以生成 []byte 类型的二进制串。只要正常使用 jsonvalue,是不会产生 error 的,因此可以直接采用 b := j.MustMarshal()

对于需要直接获得 string 类型的序列化结果的情况,则使用 s := j.MustMarshalString(),由于内部是使用 bytes.Buffer 直接输出,可以减少 string(b) 转换带来的额外耗时。


jsonvalue 性能测试

我对 jsonvalue、预定义的 structmap[string]interface{} 三种模式进行了对比,简单地将整型、浮点、字符串、数组、对象集中类型混搭和嵌套,测试结果如下:

Unmarshal 操作对比

数据类型循环次数每循环耗时每循环内存占用每循环 allocs 数
map[string]interface{}100000011357 ns4632 字节132 次
struct100000010966 ns1536 字节49 次
jsonvalue100000010711 ns7760 字节113 次

Marshal 操作对比

数据类型循环次数每循环耗时每循环内存占用每循环 allocs 数
map[string]interface{}80612615028 ns5937 字节121 次
struct39103633089 ns640 字节1 次
jsonvalue29029114115 ns2224 字节5 次

可以看到,jsonvalue 在反序列化的效率比 struct 和 map 方案均略强一点;在序列化上,struct 和 jsonvalue 远远将 map 方案抛在身后,其中 jsonvalue 耗时比 struct 多出约 1/3。综合来看,jsonvalue 的反序列化+序列化耗时比 struct 多出 5.5% 左右。毕竟 jsonvalue 处理的是不确定格式的 Json,这个成绩其实已经比较可以了。

上文所述的测试命令为 go test -bench=. -run=none -benchmem -benchtime=10s,CPU 为第十代 i5 2GHz。
读者可以参见我的 benchmark 文件


Jsonvalue 的其他高级参数

除了上述基本操作之外,jsonvalue 在序列化时还支持一些 map 方案所无法实现的功能。笔者过段时间再把这些内容另文记录吧。读者也可以参照 jsonvalue 的 godoc,文档中有详细说明。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,欢迎转载,但请注明出处。

原文标题:还在用 map[string]interface{} 处理 JSON?告诉你一个更高效的方法——jsonvalue

发布日期:2020-08-10

原文发布于云+社区,也是本人的博客

阅读 540

推荐阅读

曾经的嵌入式 / 后台开发一枚,现在开始走向架构。本专栏没有高深技术,只讲基础组件、工具,请放心食用

76 人关注
83 篇文章
专栏主页