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 框架中,內含更多實用工具與特性。



























