乐趣区

关于golang:还在用-mapstringinterface-处理-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{} 1000000 11357 ns 4632 字节 132 次
struct 1000000 10966 ns 1536 字节 49 次
jsonvalue 1000000 10711 ns 7760 字节 113 次

Marshal 操作比照

数据类型 循环次数 每循环耗时 每循环内存占用 每循环 allocs 数
map[string]interface{} 806126 15028 ns 5937 字节 121 次
struct 3910363 3089 ns 640 字节 1 次
jsonvalue 2902911 4115 ns 2224 字节 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

原文公布于云 + 社区,也是自己的博客

退出移动版