Go 的json 解析標準庫竟然存在這樣的陷阱?
日常工作中,最常用的資料傳輸格式就是json
,而encoding/json
庫是內建做解析的庫。這一節來看看它的用法,還有幾個日常使用中隱晦的陷阱和處理技巧。
-
json 與 struct
-
解析
-
反解析
-
陷阱 1、忘記取地址
-
陷阱 2、大小寫
-
陷阱 3、十六進位制或其他非 UTF8 字串
-
陷阱 4、數字轉 interface{}
-
神技、版本變更相容
-
小結
json 與 struct
一個常見的介面返回內容如下:
{
"data":{
"items":[
{
"_id":2
}
],
"total_count":1
},
"message":"",
"result_code":200
}
在golang
中往往是要把json
格式轉換成結構體物件使用的。
在新版Goland
貼上json
會自動生成結構體,也可以在網上搜到現成的工具完成自動轉換。
typeResponseDatastruct{
Datastruct{
Items[]struct{
Idint`json:"_id"`
}`json:"items"`
TotalCountint`json:"total_count"`
}`json:"data"`
Messagestring`json:"message"`
ResultCodeint`json:"result_code"`
}
用反斜槓加註解的方式表明屬於json
中哪個欄位,要注意不應該巢狀層數過多,否則難以閱讀容易出錯。
一般把內部結構體提出來,方便其他業務另做他用。
typeResponseDatastruct{
Datastruct{
Items[]Body`json:"items"`
TotalCountint64`json:"total_count"`
}`json:"data"`
Messagestring`json:"message"`
ResultCodeint64`json:"result_code"`
}
typeBodystruct{
IDint`json:"_id"`
}
解析
解析就是把json
字串轉成struct
型別。如下,第一個引數為位元組陣列,第二個為接收的結構體實體地址。如有報錯返回錯誤資訊,如沒有返回nil
//函式簽名
funcUnmarshal(data[]byte,vinterface{})error
//用法
err:=json.Unmarshal([]byte(jsonStr),&responseData)
完整程式碼如下
funcfoo(){
jsonStr:=`{"data":{"items":[{"_id":2}],"total_count":1},"message":"","result_code":200}`
//把string解析成struct
varresponseDataResponseData
err:=json.Unmarshal([]byte(jsonStr),&responseData)
iferr!=nil{
fmt.Println("parseJsonerror:"+err.Error())
return
}
fmt.Println(responseData)
}
輸出如下,和java
的toString
不同,go
會直接輸出了值,如有需要要自行實現並繫結ToString
方法。
{{[{2}]1}200}
反解析
第一步,複習初始化結構體的方法。
r:=ResponseData{
Data:struct{
Items[]Body`json:"items"`
TotalCountint64`json:"total_count"`
}{
Items:[]Body{
{ID:1},
{ID:2},
},
TotalCount:1,
},
Message:"",
ResultCode:200,
}
如上,無型別的結構體Data
需要明確把型別再寫一遍,再為其賦值。[]Body
因為是列表型別,內部如上賦值即可。
反解析函式簽名如下,傳入結構體,返回編碼好的[]byte
,和可能的報錯資訊。
funcMarshal(vinterface{})([]byte,error)
完整程式碼如下
funcbar(){
r:=ResponseData{
....
}
//把struct編譯成string
resBytes,err:=json.Marshal(r)
iferr!=nil{
fmt.Println("convertJsonerror:"+err.Error())
}
fmt.Println(string(resBytes))
}
輸出
{"data":{"items":[{"_id":1},{"_id":2}],"total_count":1},"message":"","result_code":200}
陷阱 1、忘記取地址
解析的程式碼在結尾處應該是&responseData)
忘記取地址會導致無法賦值成功,返回報錯。
err:=json.Unmarshal([]byte(jsonStr),responseData)
輸出報錯
json:Unmarshal(non-pointermain.ResponseData)
陷阱 2、大小寫
定義一個簡單的結構體來演示這個陷阱。
typePeoplestruct{
Namestring`json:"name"`
ageint`json:"age"`
}
變數如果需要被外部使用,也就是java
中的public
許可權,定義時首字母必須用大寫,這也是Go
約定的許可權控制。
typePeoplestruct
要用來解析json
的struct
內部,假如使用了小寫作為變數名,會導致無法解析成功,而且不會報錯!
funcerr1(){
reqJson:=`{"name":"minibear2333","age":26}`
varpersonPeople
err:=json.Unmarshal([]byte(reqJson),&person)
iferr!=nil{...}
fmt.Println(person)
}
輸出 0,沒有成功取到age
欄位。
{minibear23330}
這是因為標準庫中是使用反射來獲取的,私有欄位是無法獲取到的,原始碼內部不知道有這個欄位,自然無法顯示報錯資訊。
我以前沒有用自動解析,手敲上去結構體,很容易出現這樣的問題,把某個欄位首字母弄成小寫。好在編譯器會有提示。
陷阱 3、十六進位制或其他非 UTF8 字串
Go
預設使用的字串編碼是UTF8
編碼的。直接解析會出錯
funcerr2(){
raw:=[]byte(`{"name":"\xc2"}`)
varpersonPeople
iferr:=json.Unmarshal(raw,&person);err!=nil{
fmt.Println(err)
}
}
輸出
invalidcharacter'x'instringescapecode
要特別注意,加上反斜槓轉義可以成功,或者使用base64
編碼成字串,這下子單元測試的重要性就體現出來了。如下:
raw:=[]byte(`{"name":"\\xc2"}`)
raw:=[]byte(`{"name":"wg=="}`)
其他需要注意的是編碼如果不是UTF-8
格式,那麼Go
會用�
(U+FFFD
) 來代替無效的 UTF8,這不會報錯,但是獲得的字串可能不是你需要的結果。
陷阱 4、數字轉 interface{}
因為預設編碼無型別數字視為float64
。如果想用型別判斷語句為int
會直接panic
。
funcerr4(){
vardata=[]byte(`{"age":26}`)
varresultmap[string]interface{}
...
varstatus=result["age"].(int)//error
}
- 上面的程式碼隱含一個知識點,
json
中value
是簡單型別時,可以直接解析成字典。 - 如果有巢狀,那麼內部型別也會解析成字典。
- 解析成字典,輸出的時候有類似
ToString
的效果。
執行時 Panic:
panic:interfaceconversion:interface{}isfloat64,notint
goroutine1[running]:
main.err4()
- 可以先轉換成
float64
再轉換成int
- 其實還有幾種方法,太麻煩了也沒有必要,就不做特別介紹了。
神技、版本變更相容
你有沒有遇到過一種場景,一個介面更新了版本,把json
的某個欄位變更了,在請求的時候每次都定義兩套struct
。
比如Age
在版本 1 中是int
在版本 2 中是string
,解析的過程中就會出錯。
json:cannotunmarshalnumberintoGostructfieldPeople.ageoftypestring
我在下面介紹一個技巧,可以省去每次解析都要轉換的工作。
我在原始碼裡面看到,無論反射獲得的是哪種型別都會去呼叫相應的解析介面UnmarshalJSON
。
結合前面的知識,在Go
裡面看起來像鴨子就是鴨子,我們只要實現這個方法,並繫結到結構體物件上,就可以讓原始碼來呼叫我們的方法。
typePeoplestruct{
Namestring`json:"name"`
Ageint`json:"_"`
}
func(p*People)UnmarshalJSON(b[]byte)error{
...
}
- 使用下劃線表示此型別不解析。
- 必須用指標的方式繫結方法。
- 必須與 interface{}中定義的方法簽名完全一致。
一共有四個步驟
1、定義臨時型別。用來接受非json:"_"
的欄位,注意用的是type
關鍵字。
typetmpPeople
2、用中間變數接收 json 串,tmp 以外的欄位用來接受json:"_"
屬性欄位
vars=&struct{
tmp
//interface{}型別,這樣才可以接收任意欄位
Ageinterface{}`json:"age"`
}{}
//解析
err:=json.Unmarshal(b,&s)
3、判斷真實型別,並型別轉換
switcht:=s.Age.(type){
casestring:
varageint
age,err=strconv.Atoi(t)
iferr!=nil{...}
s.tmp.Age=age
casefloat64:
s.tmp.Age=int(t)
}
4、tmp 型別轉換回 People,並賦值
*p=People(s.tmp)
小結
通過本節,我們掌握了標準庫中json
解析和反解析的方法,以及很有可能日常工作中踩到的幾個坑。它們是:
- 陷阱 1、忘記取地址
- 陷阱 2、大小寫
- 陷阱 3、十六進位制或其他非 UTF8 字串
- 陷阱 4、數字轉 interface{}
- 版本變數時相容技巧