Go:終于有了處理未定義字段的實(shí)用方案
眾所周知,Go 里沒有 undefined,只有各類型的零值。多年來,Go 開發(fā)者一直依賴 JSON 結(jié)構(gòu)標(biāo)簽 omitempty 來解決“字段可能缺失”這一需求。
然而omitempty 并不能覆蓋所有場景,而且常常讓人抓狂——到底什么算“空”?定義本就含糊不清。
在 編碼(marshal) 時:
- 切片和 map 只有在為 nil 或長度為 0 時才算空。
- 指針只有 nil 時為空。
- 結(jié)構(gòu)體永遠(yuǎn)不算空。
- 字符串長度為 0 時為空。
- 其余類型為各自的零值時為空。
而在 解碼(unmarshal) 時……你根本無法區(qū)分:
- 輸入里根本沒有這個字段,還是該字段存在且值正好是 Go 的零值。
- omitempty 需要考慮的情況太多,既不方便又容易出錯。
常見變通辦法
社區(qū)常見的權(quán)宜之計(jì)是對“可能缺失”的字段統(tǒng)統(tǒng)用指針類型,并配合 omitempty:
- 編碼時,nil 字段一定不會寫進(jìn)輸出。
- 解碼時,字段若為 nil,即可判斷輸入里沒有此字段。
但這并不完美。當(dāng)你需要“可空值”(null 本身就是業(yè)務(wù)允許的合法值)時,一切又回到原點(diǎn):
- 解碼時無法分辨字段缺失還是值為 null(Go 對應(yīng) nil)。
- 編碼時若繼續(xù)用 omitempty,那么值為 nil 的字段又會被省略。
此外,大量指針也意味著到處都是判空和解引用,繁瑣且易出錯。
解決方案
隨著 Go 1.24 引入 omitzero 標(biāo)簽,我們終于可以優(yōu)雅地解決這一切。
omitzero 比 omitempty 簡單得多:字段若為零值就被省略。它同樣適用于結(jié)構(gòu)體——當(dāng)且僅當(dāng)其所有字段都是零值時才算零。
舉個例子,想省略零值的 time.Time 字段,如今只需:
type MyStruct struct {
SomeTime time.Time `json:",omitzero"`
}
再也不會輸出 0001-01-01T00:00:00Z 了!不過仍有遺留難題:
- 編碼時如何處理“可空值”?
- 如何區(qū)分“零值”與“未定義”?
- 解碼時如何區(qū)分 null 與字段缺失?
Undefined 包裝類型
得益于 omitzero 對結(jié)構(gòu)體的支持,我們可以設(shè)計(jì)一個通用包裝類型來一次性解決以上問題。思路:利用結(jié)構(gòu)體“零值”+omitzero 標(biāo)簽。
type Undefined[T any] struct {
Val T // 實(shí)際值
Present bool// 標(biāo)記字段是否出現(xiàn)
}
只要 Present 設(shè)為 true,結(jié)構(gòu)體就不再是零值;由此我們便能確定“字段已出現(xiàn)”。再實(shí)現(xiàn) json.Marshaler 與 json.Unmarshaler 接口,使其按預(yù)期工作:
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 根本不會被調(diào)用,Present 仍為 false → “未定義”。
- 若字段存在(哪怕值為 null/零值),我們會運(yùn)行 UnmarshalJSON 并把 Present 設(shè)為 true → “已出現(xiàn)”。
- 編碼時只輸出 Val 本身;若 Present=false,omitzero 會令其整體被省略。
- IsZero() 讓標(biāo)準(zhǔn)庫更高效地判斷零值。
泛型參數(shù) T 使其能包裝任何類型,一勞永逸。
進(jìn)一步擴(kuò)展
同理也可實(shí)現(xiàn)數(shù)據(jù)庫掃描(sql.Scanner)接口——這樣就能區(qū)分列是否被查詢出來。完整實(shí)現(xiàn)已收錄在 Goyave 框架中,內(nèi)含更多實(shí)用工具與特性。