還在用 map[string]interface{} 處理 JSON?告訴你一個更高效的方法——jsonvalue

本文介紹的是 jsonvalue 庫,這是我我的在 Github 上開發的第一個功能比較多而全的 Go 庫。目前主要是在騰訊將來社區的開發中使用,用於取代 map[string]interface{}git


爲何開發這個庫?

Go 是後臺開發的新銳。Go 工程師們早期就會接觸到 "encoding/json" 庫:對於已知格式的 JSON 數據,Go 的典型方法是定義一個 struct 來序列化和反序列化 (marshal/unmarshal)。github

可是對於未知格式,亦或者是不方便固定格式的情形,典型的解決方法是採用 map[string]interface{} 來處理。可是在實際應用中,這個方案是存在一些不足的。json


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
    }

不知道讀者是什麼感受,反正我是要掀桌了……

不知道讀者是什麼感受,反正我是要掀桌了……jsonp

Marshal() 效率較低

Unmarshal() 中,map[string]interface{} 類型的反序列化效率比 struct 略低一點,但大體至關。但在 Marshal() 的時候,二者的差異就很是明顯了。根據後文的一個測試方案,map 的耗時是 struct 的五倍左右。一個序列化/反序列化操做下來,就要多耗費一倍的時間。spa


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

原文發佈於雲+社區,也是本人的博客

相關文章
相關標籤/搜索