Go標準庫:Json解析陷阱與版本變動時的偷懶技巧
本文轉(zhuǎn)載自微信公眾號「機智的程序員小熊」,作者小熊。轉(zhuǎn)載本文請聯(lián)系機智的程序員小熊公眾號。
日常工作中,最常用的數(shù)據(jù)傳輸格式就是json,而encoding/json庫是內(nèi)置做解析的庫。這一節(jié)來看看它的用法,還有幾個日常使用中隱晦的陷阱和處理技巧。
- json 與 struct
- 解析
- 反解析
- 陷阱 1、忘記取地址
- 陷阱 2、大小寫
- 陷阱 3、十六進制或其他非 UTF8 字符串
- 陷阱 4、數(shù)字轉(zhuǎn) interface{}
- 神技、版本變更兼容
- 小結(jié)
json 與 struct
一個常見的接口返回內(nèi)容如下:
- {
- "data": {
- "items": [
- {
- "_id": 2
- }
- ],
- "total_count": 1
- },
- "message": "",
- "result_code": 200
- }
在golang中往往是要把json格式轉(zhuǎn)換成結(jié)構(gòu)體對象使用的。
在新版Goland粘貼json會自動生成結(jié)構(gòu)體,也可以在網(wǎng)上搜到現(xiàn)成的工具完成自動轉(zhuǎn)換。
- type ResponseData struct {
- Data struct {
- Items []struct {
- Id int `json:"_id"`
- } `json:"items"`
- TotalCount int `json:"total_count"`
- } `json:"data"`
- Message string `json:"message"`
- ResultCode int `json:"result_code"`
- }
用反斜杠加注解的方式表明屬于json中哪個字段,要注意不應該嵌套層數(shù)過多,否則難以閱讀容易出錯。
一般把內(nèi)部結(jié)構(gòu)體提出來,方便其他業(yè)務另做他用。
- type ResponseData struct {
- Data struct {
- Items []Body `json:"items"`
- TotalCount int64 `json:"total_count"`
- } `json:"data"`
- Message string `json:"message"`
- ResultCode int64 `json:"result_code"`
- }
- type Body struct {
- ID int `json:"_id"`
- }
解析
解析就是把json字符串轉(zhuǎn)成struct類型。如下,第一個參數(shù)為字節(jié)數(shù)組,第二個為接收的結(jié)構(gòu)體實體地址。如有報錯返回錯誤信息,如沒有返回nil。
- //函數(shù)簽名
- func Unmarshal(data []byte, v interface{}) error
- // 用法
- err := json.Unmarshal([]byte(jsonStr), &responseData)
完整代碼如下
- func foo() {
- jsonStr := `{"data":{"items":[{"_id":2}],"total_count":1},"message":"","result_code":200}`
- //把string解析成struct
- var responseData ResponseData
- err := json.Unmarshal([]byte(jsonStr), &responseData)
- if err != nil {
- fmt.Println("parseJson error:" + err.Error())
- return
- }
- fmt.Println(responseData)
- }
輸出如下,和java的toString不同,go會直接輸出了值,如有需要要自行實現(xiàn)并綁定ToString方法。
- {{[{2}] 1} 200}
反解析
第一步,復習初始化結(jié)構(gòu)體的方法。
- r := ResponseData{
- Data: struct {
- Items []Body `json:"items"`
- TotalCount int64 `json:"total_count"`
- }{
- Items: []Body{
- {ID: 1},
- {ID: 2},
- },
- TotalCount: 1,
- },
- Message: "",
- ResultCode: 200,
- }
如上,無類型的結(jié)構(gòu)體Data需要明確把類型再寫一遍,再為其賦值。[]Body因為是列表類型,內(nèi)部如上賦值即可。
反解析函數(shù)簽名如下,傳入結(jié)構(gòu)體,返回編碼好的[]byte,和可能的報錯信息。
- func Marshal(v interface{}) ([]byte, error)
完整代碼如下
- func bar() {
- r := ResponseData{
- ....
- }
- //把struct編譯成string
- resBytes, err := json.Marshal(r)
- if err != nil {
- fmt.Println("convertJson error: " + err.Error())
- }
- fmt.Println(string(resBytes))
- }
輸出
- {"data":{"items":[{"_id":1},{"_id":2}],"total_count":1},"message":"","result_code":200}
陷阱 1、忘記取地址
解析的代碼在結(jié)尾處應該是&responseData) 忘記取地址會導致無法賦值成功,返回報錯。
- err := json.Unmarshal([]byte(jsonStr), responseData)
輸出報錯
- json: Unmarshal(non-pointer main.ResponseData)
陷阱 2、大小寫
定義一個簡單的結(jié)構(gòu)體來演示這個陷阱。
- type People struct {
- Name string `json:"name"`
- age int `json:"age"`
- }
變量如果需要被外部使用,也就是java中的public權(quán)限,定義時首字母必須用大寫,這也是Go約定的權(quán)限控制。
- type People struct
要用來解析json的struct內(nèi)部,假如使用了小寫作為變量名,會導致無法解析成功,而且不會報錯!
- func err1() {
- reqJson := `{"name":"minibear2333","age":26}`
- var person People
- err := json.Unmarshal([]byte(reqJson), &person)
- if err != nil {...}
- fmt.Println(person)
- }
輸出 0,沒有成功取到age字段。
- {minibear2333 0}
這是因為標準庫中是使用反射來獲取的,私有字段是無法獲取到的,源碼內(nèi)部不知道有這個字段,自然無法顯示報錯信息。
我以前沒有用自動解析,手敲上去結(jié)構(gòu)體,很容易出現(xiàn)這樣的問題,把某個字段首字母弄成小寫。好在編譯器會有提示。
陷阱 3、十六進制或其他非 UTF8 字符串
Go 默認使用的字符串編碼是 UTF8 編碼的。直接解析會出錯
- func err2() {
- raw := []byte(`{"name":"\xc2"}`)
- var person People
- if err := json.Unmarshal(raw, &person); err != nil {
- fmt.Println(err)
- }
- }
輸出
- invalid character 'x' in string escape code
要特別注意,加上反斜杠轉(zhuǎn)義可以成功,或者使用base64編碼成字符串,這下子單元測試的重要性就體現(xiàn)出來了。如下:
- raw := []byte(`{"name":"\\xc2"}`)
- raw := []byte(`{"name":"wg=="}`)
其他需要注意的是編碼如果不是UTF-8格式,那么Go會用 ? (U+FFFD) 來代替無效的 UTF8,這不會報錯,但是獲得的字符串可能不是你需要的結(jié)果。
陷阱 4、數(shù)字轉(zhuǎn) interface{}
因為默認編碼無類型數(shù)字視為 float64 。如果想用類型判斷語句為int會直接panic。
- func err4() {
- var data = []byte(`{"age": 26}`)
- var result map[string]interface{}
- ...
- var status = result["age"].(int) //error
- }
- 上面的代碼隱含一個知識點,json中value是簡單類型時,可以直接解析成字典。
- 如果有嵌套,那么內(nèi)部類型也會解析成字典。
- 解析成字典,輸出的時候有類似ToString的效果。
運行時 Panic:
- panic: interface conversion: interface {} is float64, not int
- goroutine 1 [running]:
- main.err4()
可以先轉(zhuǎn)換成float64再轉(zhuǎn)換成int
其實還有幾種方法,太麻煩了也沒有必要,就不做特別介紹了。
神技、版本變更兼容
你有沒有遇到過一種場景,一個接口更新了版本,把json的某個字段變更了,在請求的時候每次都定義兩套struct。
比如Age在版本 1 中是int在版本 2 中是string,解析的過程中就會出錯。
- json: cannot unmarshal number into Go struct field People.age of type string
我在下面介紹一個技巧,可以省去每次解析都要轉(zhuǎn)換的工作。
我在源碼里面看到,無論反射獲得的是哪種類型都會去調(diào)用相應的解析接口UnmarshalJSON。
結(jié)合前面的知識,在Go里面看起來像鴨子就是鴨子,我們只要實現(xiàn)這個方法,并綁定到結(jié)構(gòu)體對象上,就可以讓源碼來調(diào)用我們的方法。
- type People struct {
- Name string `json:"name"`
- Age int `json:"_"`
- }
- func (p *People) UnmarshalJSON(b []byte) error {
- ...
- }
- 使用下劃線表示此類型不解析。
- 必須用指針的方式綁定方法。
- 必須與 interface{}中定義的方法簽名完全一致。
一共有四個步驟
1、定義臨時類型。用來接受非json:"_"的字段,注意用的是type關鍵字。
- type tmp People
2、用中間變量接收 json 串,tmp 以外的字段用來接受json:"_"屬性字段
- var s = &struct {
- tmp
- // interface{}類型,這樣才可以接收任意字段
- Age interface{} `json:"age"`
- }{}
- // 解析
- err := json.Unmarshal(b, &s)
3、判斷真實類型,并類型轉(zhuǎn)換
- switch t := s.Age.(type) {
- case string:
- var age int
- age, err = strconv.Atoi(t)
- if err != nil {...}
- s.tmp.Age = age
- case float64:
- s.tmp.Age = int(t)
- }
4、tmp 類型轉(zhuǎn)換回 People,并賦值
- *p = People(s.tmp)
小結(jié)
通過本節(jié),我們掌握了標準庫中json解析和反解析的方法,以及很有可能日常工作中踩到的幾個坑。它們是:
- 陷阱 1、忘記取地址
- 陷阱 2、大小寫
- 陷阱 3、十六進制或其他非 UTF8 字符串
- 陷阱 4、數(shù)字轉(zhuǎn) interface{}
版本變量時兼容技巧
最后分享的技巧在實際使用中,更加靈活。
留一個作業(yè):假如有v1和v2不同的兩個版本json幾乎完成不同,業(yè)務邏輯已經(jīng)使用v1版本,是否可以把v2版本轉(zhuǎn)換成v1版本,幾乎不用改動業(yè)務邏輯?
提示:可以通過深拷貝把v2版本解析出來的結(jié)構(gòu)體完全轉(zhuǎn)換成v1版本的結(jié)構(gòu)體。
要求:必須使用實現(xiàn) UnmarshalJSON的技巧。
本文轉(zhuǎn)載自微信公眾號「機智的程序員小熊」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系機智的程序員小熊公眾號。