1. 程式人生 > 其它 >Go 的json 解析標準庫竟然存在這樣的陷阱?

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)
}

輸出如下,和javatoString不同,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

要用來解析jsonstruct內部,假如使用了小寫作為變數名,會導致無法解析成功,而且不會報錯!

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
}
  • 上面的程式碼隱含一個知識點,jsonvalue是簡單型別時,可以直接解析成字典。
  • 如果有巢狀,那麼內部型別也會解析成字典。
  • 解析成字典,輸出的時候有類似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{}
  • 版本變數時相容技巧