在 Go 语言的日常开发中,JSON 序列化是我们再熟悉不过的操作了。相信大家都用过 omitempty 标签来忽略空值字段,但你有没有遇到过这些尴尬场景:

  • time.Time 类型的零值 "0001-01-01T00:00:00Z" 明明想忽略,却总是被序列化出来
  • 空切片 []string{}nil 切片想要区别对待,却无能为力
  • 自定义类型想要定义自己的"零值"规则,却找不到入口

这些问题,在 Go 1.24 版本中终于得到了完美的解决方案——omitzero 标签横空出世!

认识 omitzero

omitzeroGo 1.24encoding/json 包中新增的结构体标签选项。它的作用是在序列化 JSON 时,忽略零值字段

等等,零值?这和 omitempty 的空值有什么区别?

问得好!这正是 omitzero 的核心价值所在。

零值 vs 空值

在 Go 语言中,零值(zero value)空值(empty value)虽然听起来相似,但实际上并不等价:

// 空值(omitempty 判断标准)
""           // 空字符串
0            // 数字零
nil          // 空指针
[]           // 空切片
false        // 布尔假值

// 零值(omitzero 判断标准)
time.Time{}  // 时间零值:0001-01-01T00:00:00Z
customType{} // 自定义类型的零值

看出来了吗?time.Time 的零值是一个具体的时间戳,而不是空字符串,所以 omitempty 拿它没办法!

核心应用场景

time.Time 的零值处理

这是最经典的场景。来看代码:

type User struct {
    Name   string    `json:"name"`
    BornAt time.Time `json:"born_at,omitempty"`
}

func main() {
    user := User{Name: "张三"}
    // BornAt 是零值,但 omitempty 不会忽略它
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
}

输出结果:

{"name":"张三","born_at":"0001-01-01T00:00:00Z"}

看到了吗?即使使用了 omitemptyborn_at 字段还是被序列化出来了,而且是一个毫无意义的零值时间。

使用 omitzero 解决:

type User struct {
    Name   string    `json:"name"`
    BornAt time.Time `json:"born_at,omitzero"`
}

现在,零值的 BornAt 会被正确忽略,输出:

{"name":"张三"}

空切片 vs nil 切片的区别对待

有时候,我们想要区分 nil 切片和空切片:

type User struct {
    Hobbies []string `json:"hobbies,omitempty"`
}

使用 omitempty 时,无论是 nil 还是 []string{} 都会被忽略。但有时候,我们只想忽略 nil,而保留空数组 []

使用 omitzero 解决:

type User struct {
    Hobbies []string `json:"hobbies,omitzero"`
}

func main() {
    // nil 切片会被忽略
    user1 := User{Hobbies: nil}
    data1, _ := json.Marshal(user1)
    // 输出:{}

    // 空切片会被保留
    user2 := User{Hobbies: []string{}}
    data2, _ := json.Marshal(user2)
    // 输出:{"hobbies":[]}
}

这样就能精确控制什么时候输出空数组,什么时候完全忽略字段了。

自定义零值判断逻辑

这是 omitzero 最强大的功能——通过实现 IsZero() 方法,你可以自定义什么是"零值"。

来看一个实际例子:

// 自定义年龄类型,负数和 0 都视为零值
type Age int

func (age *Age) IsZero() bool {
    return *age <= 0
}

type User struct {
    Name string `json:"name"`
    Age  Age    `json:"age,omitzero"`
}

func main() {
    user := User{
        Name: "张三",
        Age:  -1,  // 负数,会被 IsZero() 判断为零值
    }
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
}

输出:

{"name":"张三"}

age 字段因为 IsZero() 返回 true 而被忽略了。这个功能在处理业务逻辑复杂的场景时特别有用。

与 omitempty 对比

让我们系统地对比一下这两个标签:

特性 omitempty omitzero
忽略条件 空值(empty) 零值(zero)
time.Time 不忽略零值时间 忽略零值时间
空切片 忽略 []nil 只忽略 nil
自定义判断 ❌ 不支持 ✅ 支持 IsZero()
适用场景 常规空值忽略 精确零值控制

实践指南

type Data struct {
    Field1 string    `json:"field1,omitempty"`
    Field2 string    `json:"field2,omitzero"`
    Time1  time.Time `json:"time1,omitempty"`
    Time2  time.Time `json:"time2,omitzero"`
    Slice1 []int     `json:"slice1,omitempty"`
    Slice2 []int     `json:"slice2,omitzero"`
}

func main() {
    data := Data{
        Field1: "",           // 空字符串
        Field2: "",           // 空字符串
        Time1:  time.Time{},  // 零值时间
        Time2:  time.Time{},  // 零值时间
        Slice1: []int{},      // 空切片
        Slice2: []int{},      // 空切片
    }

    json.MarshalIndent(data, "", "  ")
}

输出结果:

{
  "time1": "0001-01-01T00:00:00Z",
  "slice2": []
}

看到了吗?

  • field1field2 都是空字符串,都被忽略了
  • time1 是零值但不是空值,所以没被忽略;time2 是零值,被忽略了
  • slice1 是空切片被视为空值被忽略;slice2 是空切片但不是零值,所以保留了

使用 omitzero 的场景:

  • 处理时间类型time.Time 的零值处理是 omitzero 最典型的应用场景
  • 区分 nil 和空集合:需要保留空数组 [] 但忽略 nil 时使用
  • 自定义零值判断:通过 IsZero() 方法实现业务特定的零值逻辑

继续使用 omitempty 的场景:

  • 常规字符串字段:空字符串需要被忽略的场景
  • 指针类型nil 指针需要被忽略的场景
  • 简单的布尔和数字字段false0 需要被忽略的场景

简单记忆:需要精确控制零值用 omitzero,常规空值忽略用 omitempty

写在最后

omitzero 的引入,标志着 Go 语言的 JSON 序列化能力迈上了新的台阶。它不仅解决了长期存在的 time.Time 零值处理问题,还通过 IsZero() 方法提供了强大的自定义能力。

可以预见,omitzero 可能会成为更多开发者的首选。它让 Go 的 JSON 序列化更加精细和灵活,特别是在处理复杂业务场景时。