别再只用 omitempty 了:Go 1.24 的 omitzero 让 JSON 序列化更精确

星野
星野
发布时间:2026-08-25 | 177人浏览过
原创
暖色专栏手绘图:omitempty 与 omitzero 的选择规则和字段适配建议
什么时候继续用 omitempty,什么时候把两种标签适合的字段类型做成专栏式选择卡,便于读者快速落地。

前言

在 Go 项目里,很多人已经把 `omitempty` 用成了默认配置,但一到 `time.Time`、空切片和自定义类型这些细一点的场景,序列化结果就常常不如预期。Go 1.24 新增的 `omitzero`,正好补上了这块缺口:它不是替代 `omitempty`,而是把“该不该输出”这件事拆得更清楚。读完这篇,你可以直接判断两者分别适合什么字段,以及什么时候该让 `IsZero()` 接管业务规则。

在 Go 项目里,很多人已经把 omitempty 用成了默认配置,但一到 time.Time、空切片和自定义类型这些细一点的场景,序列化结果就常常不如预期。Go 1.24 新增的 omitzero,正好补上了这块缺口:它不是替代 omitempty,而是把“该不该输出”这件事拆得更清楚。读完这篇,你可以直接判断两者分别适合什么字段,以及什么时候该让 IsZero() 接管业务规则。

Go 1.24 的 omitzero 到底解决了什么

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

这看上去和 omitempty 很像,但两者判断的不是同一件事。omitempty 关注的是“空值”,而 omitzero 关注的是“零值”。这也是它能解决老问题的关键。

零值和空值不是一回事

在 Go 里,这两个概念很容易混在一起,但实际行为并不相同:

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

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

最典型的例子就是 time.Time。它的零值不是空字符串,也不是 nil,而是一个具体可序列化的时间值,所以仅靠 omitempty 根本拦不住它。

三个最常见的使用场景

1. 处理 time.Time 的零值输出

这是 omitzero 最直接、也最容易遇到的场景。

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"}

这里的 born_at 虽然没实际业务意义,但依然会出现在 JSON 中。对于接口返回、日志记录甚至前端展示,这通常都不是想要的结果。

改成 omitzero 后就符合直觉了:

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

此时零值的 BornAt 会被省略,输出变成:

{"name":"张三"}

如果你的结构体里有创建时间、出生日期、过期时间这类字段,而这些字段允许“未设置”,那 omitzero 基本就是更准确的选择。

2. 区分 nil 切片和空切片

另一个高频问题是切片字段。很多接口会把“字段不存在”和“字段存在但为空数组”当成两种不同含义,但 omitempty 往往会把它们一起省略。

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":[]}
}

这样一来,接口语义就更稳定了:nil 表示省略,空切片表示明确返回空数组。

3. 用 IsZero() 自定义“该忽略什么”

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() 明确规定了“<= 0 都算零值”。这对有状态枚举、业务型数值、包装类型都很实用。

和 omitempty 放在一起看,差别更清楚

如果只记一个结论,那就是:omitempty 用来处理常规空值,omitzero 用来处理更精确的零值控制。

特性 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": []
}

这里能看出几个关键差异:

  • field1 和 field2 都是空字符串,因此都被忽略
  • time1 是零值,但不属于 omitempty 的空值判断,所以仍被输出
  • time2 因为命中了零值判断,被 omitzero 省略
  • slice1 是空切片,被 omitempty 忽略
  • slice2 是空切片但不是零值,所以被保留下来

项目里该怎么选

omitzero 很实用,但也不是所有字段都该统一替换。更稳妥的方式,是按字段语义来选标签。

适合用 omitzero 的情况

  • 时间字段:尤其是 time.Time 的零值不该落到 JSON 里的场景
  • 需要区分 nil 和空集合:希望保留 [],但忽略 nil
  • 业务规则有自定义零值:需要通过 IsZero() 把领域逻辑带入序列化过程

继续用 omitempty 的情况

  • 常规字符串字段:空字符串就该省略
  • 指针类型:nil 指针自然不输出
  • 简单数字和布尔值:0、false 就是你想过滤掉的默认值

可以把它理解成一句更实用的经验规则:默认先想字段语义,再决定是空值优先还是零值优先。如果你真正关心的是“未初始化状态”,那就优先考虑 omitzero;如果你关心的是“空内容是否输出”,omitempty 往往就够用了。

对很多 Go 项目来说,omitzero 不只是一个新标签,它实际上让 JSON 输出更接近业务表达。尤其当结构体被接口响应、配置文件和消息体多处复用时,这种细粒度控制会比表面上看起来更有价值。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表