天天看點

Go 中“omitempty”的陷阱

作者:大資料推薦雜談

用法

熟悉 Golang 的朋友對于 json 和 struct 之間的轉換一定不陌生,為了将代碼中的結構體與 json 資料解耦,通常我們會在結構體的 field 類型後加上解釋說明,例如在表示一個位址的時候, json 資料如下所示

{
    "street": "200 Larkin St",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}
           

與之相對應的 Golang 結構體表示定義如下

type address struct {
 Street  string `json:"street"`  // 街道
 Ste     string `json:"suite"`   // 單元(可以不存在)
 City    string `json:"city"`    // 城市
 State   string `json:"state"`   // 州/省
 Zipcode string `json:"zipcode"` // 郵編
}
           

這樣無論代碼中的變量如何改變,我們都能成功将 json 資料解析出來,獲得正确的街道,城市等資訊,到目前為止一切正常。但如果我們想要将位址結構體恢複成 json 格式時,問題就來了。比方說我們用下面這段代碼讀取了位址 json ,然後根據業務邏輯處理了之後恢複成正常的 json 列印出來

func main() {
  data := `{
  "street": "200 Larkin St",
  "city": "San Francisco",
  "state": "CA",
  "zipcode": "94102"
 }`
 addr := new(address)
 json.Unmarshal([]byte(data), &addr)

        // 處理了一番 addr 變量...

 addressBytes, _ := json.MarshalIndent(addr, "", "    ")
 fmt.Printf("%s\n", string(addressBytes))
}
           

可以得到運作結果

{
    "street": "200 Larkin St",
    "suite": "",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}
           

多了一行 "suite": "", ,而這則資訊在原本的 json 資料中是沒有的(在美國的位址中,如果不是群租較高價的電梯大廈或者共享辦公樓, suite 這一條不存在很正常,人們直接用街道門牌号來表示位址就足夠了),但我們更希望的是,在一個位址有 suite 号碼的時候輸出,不存在 suite 的時候就不輸出,幸運的是,我們可以在 Golang 的結構體定義中添加 omitempty 關鍵字,來表示這條資訊如果沒有提供,在序列化成 json 的時候就不要包含其預設值。稍作修改,位址結構體就變成了

type address struct {
 Street  string `json:"street"`
 Ste     string `json:"suite,omitempty"`
 City    string `json:"city"`
 State   string `json:"state"`
 Zipcode string `json:"zipcode"`
}
           

重新運作,即可得到正确的結果。

陷阱

帶來友善的同時,使用 omitempty 也有些小陷阱,一個是該關鍵字無法忽略掉嵌套結構體。還是拿位址類型說事,這回我們想要往位址結構體中加一個新 field 來表示經緯度,如果缺乏相關的資料,暫時可以忽略。新的結構體定義如下所示

type address struct {
 Street     string     `json:"street"`
 Ste        string     `json:"suite,omitempty"`
 City       string     `json:"city"`
 State      string     `json:"state"`
 Zipcode    string     `json:"zipcode"`
 Coordinate coordinate `json:"coordinate,omitempty"`
}

type coordinate struct {
 Lat float64 `json:"latitude"`
 Lng float64 `json:"longitude"`
}
           

讀入原來的位址資料,處理後序列化輸出,我們就會發現即使加上了 omitempty 關鍵字,輸出的 json 還是帶上了一個空的坐标資訊

{
    "street": "200 Larkin St",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102",
    "coordinate": {
        "latitude": 0,
        "longitude": 0
    }
}
           

為了達到我們想要的效果,可以把坐标定義為指針類型,這樣 Golang 就能知道一個指針的“空值”是多少了,否則面對一個我們自定義的結構, Golang 是猜不出我們想要的空值的。于是有了如下的結構體定義

type address struct {
 Street     string      `json:"street"`
 Ste        string      `json:"suite,omitempty"`
 City       string      `json:"city"`
 State      string      `json:"state"`
 Zipcode    string      `json:"zipcode"`
 Coordinate *coordinate `json:"coordinate,omitempty"`
}

type coordinate struct {
 Lat float64 `json:"latitude"`
 Lng float64 `json:"longitude"`
}
           

相應的輸出為

{
    "street": "200 Larkin St",
    "city": "San Francisco",
    "state": "CA",
    "zipcode": "94102"
}
           

另一個“陷阱”是,對于用 omitempty 定義的 field ,如果給它賦的值恰好等于預設空值的話,在轉為 json 之後也不會輸出這個 field 。比如說上面定義的經緯度坐标結構體,如果我們将經緯度兩個 field 都加上 omitempty

type coordinate struct {
 Lat float64 `json:"latitude,omitempty"`
 Lng float64 `json:"longitude,omitempty"`
}
           

然後我們對非洲幾内亞灣的“原點坐标”非常感興趣,于是編寫了如下代碼

func main() {
 cData := `{
  "latitude": 0.0,
  "longitude": 0.0
 }`
 c := new(coordinate)
 json.Unmarshal([]byte(cData), &c)

  // 具體處理邏輯...

 coordinateBytes, _ := json.MarshalIndent(c, "", "    ")
 fmt.Printf("%s\n", string(coordinateBytes))
}
           

最終我們得到了一個

{}
           

這個坐标消失不見了!但我們的設想是,如果一個地點沒有經緯度資訊,則懸空,這沒有問題,但對于“原點坐标”,我們在确切知道它的經緯度的情況下,(0.0, 0.0)仍然被忽略了。正确的寫法也是将結構體内的定義改為指針

type coordinate struct {
 Lat *float64 `json:"latitude,omitempty"`
 Lng *float64 `json:"longitude,omitempty"`
}
           

這樣空值就從 float64 的 0.0 變為了指針類型的 nil ,我們就能看到正确的經緯度輸出。

{
    "latitude": 0,
    "longitude": 0
}           

繼續閱讀