Go:終於有了處理未定義字段的實用方案
衆所周知,Go 裏沒有 undefined,只有各類型的零值。多年來,Go 開發者一直依賴 JSON 結構標籤 omitempty
來解決 “字段可能缺失” 這一需求。
然而omitempty
並不能覆蓋所有場景,而且常常讓人抓狂——到底什麼算 “空”?定義本就含糊不清。
在 編碼(marshal) 時:
-
切片和 map 只有在爲
nil
或長度爲 0 時纔算空。 -
指針只有
nil
時爲空。 -
結構體永遠不算空。
-
字符串長度爲 0 時爲空。
-
其餘類型爲各自的零值時爲空。
而在 解碼(unmarshal) 時…… 你根本無法區分:
- 輸入里根本沒有這個字段,還是該字段存在且值正好是 Go 的零值。
omitempty
需要考慮的情況太多,既不方便又容易出錯。
常見變通辦法
社區常見的權宜之計是對 “可能缺失” 的字段統統用指針類型,並配合 omitempty
:
-
編碼時,
nil
字段一定不會寫進輸出。 -
解碼時,字段若爲
nil
,即可判斷輸入裏沒有此字段。
但這並不完美。當你需要 “可空值”(null
本身就是業務允許的合法值)時,一切又回到原點:
-
解碼時無法分辨字段缺失還是值爲
null
(Go 對應nil
)。 -
編碼時若繼續用
omitempty
,那麼值爲nil
的字段又會被省略。
此外,大量指針也意味着到處都是判空和解引用,繁瑣且易出錯。
解決方案
隨着 Go 1.24 引入 omitzero
標籤,我們終於可以優雅地解決這一切。
omitzero
比 omitempty
簡單得多:字段若爲零值就被省略。它同樣適用於結構體——當且僅當其所有字段都是零值時纔算零。
舉個例子,想省略零值的 time.Time
字段,如今只需:
type MyStruct struct {
SomeTime time.Time `json:",omitzero"`
}
再也不會輸出 0001-01-01T00:00:00Z
了!不過仍有遺留難題:
-
編碼時如何處理 “可空值”?
-
如何區分 “零值” 與“未定義”?
-
解碼時如何區分 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
}
-
若輸入缺少該字段,
UnmarshalJSON
根本不會被調用,Present
仍爲false
→ “未定義”。 -
若字段存在(哪怕值爲
null
/ 零值),我們會運行UnmarshalJSON
並把Present
設爲true
→ “已出現”。 -
編碼時只輸出
Val
本身;若Present=false
,omitzero
會令其整體被省略。 -
IsZero()
讓標準庫更高效地判斷零值。
泛型參數 T
使其能包裝任何類型,一勞永逸。
進一步擴展
同理也可實現數據庫掃描(sql.Scanner
)接口——這樣就能區分列是否被查詢出來。完整實現已收錄在 Goyave 框架中,內含更多實用工具與特性。
例行海報
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7nzYnWtJ5rZC4Fcd6yGxfg