本文介紹的是 jsonvalue 庫,這是我我的在 Github 上開發的第一個功能比較多而全的 Go 庫。目前主要是在騰訊將來社區的開發中使用,用於取代 map[string]interface{}
。git
Go 是後臺開發的新銳。Go 工程師們早期就會接觸到 "encoding/json"
庫:對於已知格式的 JSON 數據,Go 的典型方法是定義一個 struct
來序列化和反序列化 (marshal/unmarshal
)。github
可是對於未知格式,亦或者是不方便固定格式的情形,典型的解決方法是採用 map[string]interface{}
來處理。可是在實際應用中,這個方案是存在一些不足的。json
有一些狀況下,咱們確實須要採用 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
在 Unmarshal()
中,map[string]interface{}
類型的反序列化效率比 struct
略低一點,但大體至關。但在 Marshal()
的時候,二者的差異就很是明顯了。根據後文的一個測試方案,map
的耗時是 struct
的五倍左右。一個序列化/反序列化操做下來,就要多耗費一倍的時間。spa
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(...)
中,函數完成了如下幾個功能:
也就是說,在前面的問題中一長串的檢查,都在這個函數中自動幫你解決了。
除了 string 類型外,jsonvalue
也支持 GetBool, GetNull, GetInt, GetUint, GetInt64, GetArray, GetObject
等等一系列的類型獲取,只要你想到的 Json 類型都提供。
大部分狀況下,咱們須要編輯一個 JSON object。使用 j := jsonvalue.NewObject()
。後續能夠採用 SetXxx().At()
系列函數設置子成員。與前面所說的 GetXxx
系列函數同樣,其實 jsonvalue 也支持一站式的複雜結構生成。下面咱們一個一個說明:
好比在 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 數組添加子成員也是必要的功能。一樣地,咱們先建立一個數組:a := jsonvalue.NewArray()
。對數組的基本操做有如下幾個:
// 在數組的開頭添加元素 a.AppendString("Hello, world!").InTheBegging() // 在數組的末尾添加元素 a.AppendInt(5678).InTheEnd() // 在數組中指定位置的前面插入元素 a.InsertFloat32(3.14159).Before(1) // 在數組中指定位置的後面插入元素 a.InsertNull().After(2)
針對編輯場景,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 值。具體以下:
SetXxxx
所指定的子成員類型,建立子成員具體到上面的例子,那麼整個操做邏輯以下:
SetString()
函數表示準備設置一個 string 類型的子成員At()
函數表示開始在 JSON 對象中尋址。"Response"
參數,首先檢查到這不是最後一個參數,那麼首先判斷當前的 j
是否是一個 object 對象,若是不是,則返回 error"Response"
對象存在,則取出;如不存在,則建立,而後內部遞歸地調用 response.SetString("Hello, world!").At("Result", "AnArray", 0, "SomeString")
"Result"
同理"Result"
層的對象以後,檢查下一個參數,發現是整型,則函數判斷爲預期下一層目標 "AnArray"
應該是一個數組。那麼函數內首先獲取這個目標,若是不存在,則建立一個數組;若是存在,則若是該目標不是數組的話,會返回 error拿到 "AnArray"
以後,當前參數爲整數。這裏的邏輯比較複雜:
"SomeString"
是一個 string 類型,那麼表示 AnArray[0]
應是一個 object,則在 AnArray[0]
位置建立一個 JSON object,而且設置 {"SomeString":"Hello, world!"}
其實能夠看到,上面的流程對於目標爲數組類型來講,不太直觀。所以對於目標 JSON 爲數組的層級,前文提到的 Append
和 Insert
函數也支持不定量參數。舉個例子,若是咱們須要在上述說起的 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
、預約義的 struct
、map[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 在序列化時還支持一些 map 方案所沒法實現的功能。筆者過段時間再把這些內容另文記錄吧。讀者也能夠參照 jsonvalue 的 godoc,文檔中有詳細說明。
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原做者: amc,歡迎轉載,但請註明出處。
原文標題:還在用 map[string]interface{} 處理 JSON?告訴你一個更高效的方法——jsonvalue
發佈日期:2020-08-10
原文發佈於雲+社區,也是本人的博客