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