Go 的 json 解析標準庫竟然存在這樣的陷阱?

日常工作中,最常用的數據傳輸格式就是json,而encoding/json庫是內置做解析的庫。這一節來看看它的用法,還有幾個日常使用中隱晦的陷阱和處理技巧。

json 與 struct

一個常見的接口返回內容如下:

{
  "data"{
    "items"[
      {
        "_id"2
      }
    ],
    "total_count"1
  },
  "message""",
  "result_code"200
}

golang中往往是要把json格式轉換成結構體對象使用的。

在新版Goland粘貼json會自動生成結構體,也可以在網上搜到現成的工具完成自動轉換。

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中哪個字段,要注意不應該嵌套層數過多,否則難以閱讀容易出錯。

一般把內部結構體提出來,方便其他業務另做他用。

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字符串轉成struct類型。如下,第一個參數爲字節數組,第二個爲接收的結構體實體地址。如有報錯返回錯誤信息,如沒有返回nil

//函數簽名
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)
}

輸出如下,和javatoString不同,go會直接輸出了值,如有需要要自行實現並綁定ToString方法。

{{[{2}] 1}  200}

反解析

第一步,複習初始化結構體的方法。

r := ResponseData{
    Data: struct {
        Items      []Body `json:"items"`
        TotalCount int64  `json:"total_count"`
    }{
        Items: []Body{
            {ID: 1},
            {ID: 2},
        },
        TotalCount: 1,
    },
    Message:    "",
    ResultCode: 200,
}

如上,無類型的結構體Data需要明確把類型再寫一遍,再爲其賦值。[]Body因爲是列表類型,內部如上賦值即可。

反解析函數簽名如下,傳入結構體,返回編碼好的[]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、忘記取地址

解析的代碼在結尾處應該是&responseData) 忘記取地址會導致無法賦值成功,返回報錯。

err := json.Unmarshal([]byte(jsonStr), responseData)

輸出報錯

json: Unmarshal(non-pointer main.ResponseData)

陷阱 2、大小寫

定義一個簡單的結構體來演示這個陷阱。

type People struct {
 Name string `json:"name"`
 age  int    `json:"age"`
}

變量如果需要被外部使用,也就是java中的public權限,定義時首字母必須用大寫,這也是Go約定的權限控制。

type People struct

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

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}

這是因爲標準庫中是使用反射來獲取的,私有字段是無法獲取到的,源碼內部不知道有這個字段,自然無法顯示報錯信息。

我以前沒有用自動解析,手敲上去結構體,很容易出現這樣的問題,把某個字段首字母弄成小寫。好在編譯器會有提示。

陷阱 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

要特別注意,加上反斜槓轉義可以成功,或者使用base64編碼成字符串,這下子單元測試的重要性就體現出來了。如下:

raw := []byte(`{"name":"\\xc2"}`)
raw := []byte(`{"name":"wg=="}`)

其他需要注意的是編碼如果不是UTF-8格式,那麼Go會用 (U+FFFD) 來代替無效的 UTF8,這不會報錯,但是獲得的字符串可能不是你需要的結果。

陷阱 4、數字轉 interface{}

因爲默認編碼無類型數字視爲 float64 。如果想用類型判斷語句爲int會直接panic

func err4() {
 var data = []byte(`{"age": 26}`)
 var result map[string]interface{}
 ...
 var status = result["age"].(int) //error
}

運行時 Panic:

panic: interface conversion: interface {} is float64, not int

goroutine 1 [running]:
main.err4()

神技、版本變更兼容

你有沒有遇到過一種場景,一個接口更新了版本,把json的某個字段變更了,在請求的時候每次都定義兩套struct

比如Age在版本 1 中是int在版本 2 中是string,解析的過程中就會出錯。

json: cannot unmarshal number into Go struct field People.age of type string

我在下面介紹一個技巧,可以省去每次解析都要轉換的工作。

我在源碼裏面看到,無論反射獲得的是哪種類型都會去調用相應的解析接口UnmarshalJSON

結合前面的知識,在Go裏面看起來像鴨子就是鴨子,我們只要實現這個方法,並綁定到結構體對象上,就可以讓源碼來調用我們的方法。

type People struct {
    Name string `json:"name"`
    Age  int    `json:"_"`
}
func (p *People) UnmarshalJSON([]byte) error {
 ...
}

一共有四個步驟

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、判斷真實類型,並類型轉換

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 類型轉換回 People,並賦值

*p = People(s.tmp)

小結

通過本節,我們掌握了標準庫中json解析和反解析的方法,以及很有可能日常工作中踩到的幾個坑。它們是:

最後分享的技巧在實際使用中,更加靈活。

留一個作業:假如有v1v2不同的兩個版本json幾乎完成不同,業務邏輯已經使用v1版本,是否可以把v2版本轉換成v1版本,幾乎不用改動業務邏輯?

提示:可以通過深拷貝把v2版本解析出來的結構體完全轉換成v1版本的結構體。

要求:必須使用實現 UnmarshalJSON的技巧。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OyiF57PcMQjeoGpShEgidQ