Go:終於有了處理未定義字段的實用方案

衆所周知,Go 裏沒有 undefined,只有各類型的零值。多年來,Go 開發者一直依賴 JSON 結構標籤 omitempty 來解決 “字段可能缺失” 這一需求。

然而omitempty 並不能覆蓋所有場景,而且常常讓人抓狂——到底什麼算 “空”?定義本就含糊不清。

在 編碼(marshal) 時:

而在 解碼(unmarshal) 時…… 你根本無法區分:

omitempty 需要考慮的情況太多,既不方便又容易出錯。

常見變通辦法

社區常見的權宜之計是對 “可能缺失” 的字段統統用指針類型,並配合 omitempty

但這並不完美。當你需要 “可空值”(null 本身就是業務允許的合法值)時,一切又回到原點:

此外,大量指針也意味着到處都是判空和解引用,繁瑣且易出錯。

解決方案

隨着 Go 1.24 引入 omitzero 標籤,我們終於可以優雅地解決這一切。

omitzero 比 omitempty 簡單得多:字段若爲零值就被省略。它同樣適用於結構體——當且僅當其所有字段都是零值時纔算零。

舉個例子,想省略零值的 time.Time 字段,如今只需:

type MyStruct struct {  
    SomeTime time.Time `json:",omitzero"`  
}

再也不會輸出 0001-01-01T00:00:00Z 了!不過仍有遺留難題:

  1. 編碼時如何處理 “可空值”?

  2. 如何區分 “零值” 與“未定義”?

  3. 解碼時如何區分 null 與字段缺失?

Undefined 包裝類型

得益於 omitzero 對結構體的支持,我們可以設計一個通用包裝類型來一次性解決以上問題。思路:利用結構體 “零值”+omitzero 標籤。

type Undefined[T any] struct {  
    Val     T   // 實際值  
    Present bool// 標記字段是否出現  
}

只要 Present 設爲 true,結構體就不再是零值;由此我們便能確定 “字段已出現”。再實現 json.Marshaler 與 json.Unmarshaler 接口,使其按預期工作:

func (u *Undefined[T]) UnmarshalJSON(data []byte) error {  
    if err := json.Unmarshal(data, &u.Val); err != nil {  
        return fmt.Errorf("Undefined: 反序列化失敗: %w", err)  
    }  
    u.Present = true  
    return nil  
}  

func (u Undefined[T]) MarshalJSON() ([]byte, error) {  
    data, err := json.Marshal(u.Val)  
    if err != nil {  
        return nil, fmt.Errorf("Undefined: 序列化失敗: %w", err)  
    }  
    return data, nil  
}  

// 供 encoding/json 判斷零值  
func (u Undefined[T]) IsZero() bool {  
    return !u.Present  
}

泛型參數 T 使其能包裝任何類型,一勞永逸。

進一步擴展

同理也可實現數據庫掃描(sql.Scanner)接口——這樣就能區分列是否被查詢出來。完整實現已收錄在 Goyave 框架中,內含更多實用工具與特性。

例行海報

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