Go 的 encoding/json 实战:omitempty、零值与自定义时间格式的坑

发布时间:2026/7/21 14:32:07
Go 的 encoding/json 实战:omitempty、零值与自定义时间格式的坑 Go 的 encoding/json 实战:omitempty、零值与自定义时间格式的坑写 Go 后端接口,几乎绕不开encoding/json。但很多人踩过这些坑:该省略的字段没省略、时间格式前端看不懂、明明想传false却被omitempty吞掉。这篇把 JSON 序列化里最常见的几个真实痛点一次讲透,全部代码可直接跑。一、struct tag 的基本约定,先别记错Go 默认用字段名的原样(首字母大写)做 JSON key,前端拿到Name、UserID这种大写 key 会很别扭。用 tag 显式指定:packagemainimport(encoding/jsonfmt)typeUserstruct{IDint64json:idNamestringjson:namePasswordstringjson:-// - 表示永远不序列化,敏感字段常用Nicknamestringjson:nickname,omitempty}funcmain(){u:User{ID:1,Name:老王,Password:secret}b,_:json.Marshal(u)fmt.Println(string(b))// {id:1,name:老王} —— Password 被丢弃,Nickname 为空被 omitempty 省略}两个要点:json:-是彻底排除(密码、内部字段);omitempty是「值为零值时省略」。这俩经常被搞混。二、omitempty 的真正陷阱:它认的是「零值」不是「没设置」这是最容易线上出 bug 的地方。omitempty判断的是 Go 的零值:0、、false、nil、空 slice/map。问题来了——如果你有个开关字段Enabled bool,用户明确要传false,加了omitempty就会被吞掉:typeConfigstruct{Enabledbooljson:enabled,omitempty}c:Config{Enabled:false}b,_:json.Marshal(c)fmt.Println(string(b))// {} —— false 被当成零值省略了!前端收不到 enabled前端拿不到enabled,只能猜默认值,行为就错了。正确做法是用指针区分「没设置(nil)」和「设置成 false」:typeConfigstruct{Enabled*booljson:enabled,omitempty}funcboolPtr(bbool)*bool{returnb}c:Config{Enabled:boolPtr(false)}b,_:json.Marshal(c)fmt.Println(string(b))// {enabled:false} —— 现在 false 能正常传出去记忆点:布尔、数字这类「零值也是合法业务值」的字段,要么别加 omitempty,要么用指针。别无脑给所有字段挂 omitempty。三、时间格式:默认的 RFC3339 前端不一定认time.Time默认序列化成 RFC3339(2026-07-21T09:00:0008:00),但很多前端/移动端要的是2026-07-21 09:00:00。硬用 string 存时间又丢了类型。正确姿势是定义自定义类型,实现MarshalJSON:packagemainimport(encoding/jsonfmttime)constlayout2006-01-02 15:04:05// 自定义时间类型,包一层 time.TimetypeLocalTime time.Time// 序列化:把时间格式化成前端要的字符串func(t LocalTime)MarshalJSON()([]byte,error){// 注意要自己加引号,输出的是 JSON 字符串s:fmt.Sprintf(%q,time.Time(t).Format(layout))return[]byte(s),nil}// 反序列化:把字符串解析回时间,接收前端传入时用func(t*LocalTime)UnmarshalJSON(data[]byte)error{s:string(data)ifsnull||s { return nil } parsed, err : time.ParseInLocation(layout, s, time.Local) if err ! nil { return err } *t LocalTime(parsed) return nil } type Order struct { ID int64 json:id CreatedAt LocalTime json:created_at}funcmain(){o:Order{ID:100,CreatedAt:LocalTime(time.Now())}b,_:json.Marshal(o)fmt.Println(string(b))// {id:100,created_at:2026-07-21 09:00:00}}坑点:MarshalJSON返回的必须是合法 JSON,字符串要自己带引号(用%q或手动拼...),忘了引号会报invalid character的解析错误。四、动态/未知结构:别急着定义一堆 struct对接第三方接口,字段经常不固定。这时用map[string]any或json.RawMessage延迟解析:typeEnvelopestruct{Typestringjson:typeData json.RawMessagejson:data// 先不解析,等知道 type 再解析}funchandle(raw[]byte)error{vare Envelopeiferr:json.Unmarshal(raw,e);err!nil{returnerr}switche.Type{caseuser:varu Userreturnjson.Unmarshal(e.Data,u)// 到这里才按 User 解析// 其他 type 分支...}returnnil}json.RawMessage本质是[]byte,它让你「先看外层,再决定内层怎么解」,比一股脑map[string]any再各种类型断言干净得多。小结json:-彻底排除字段,omitempty只在零值时省略,别混用。omitempty 最大的坑是吞掉合法的false/0;这类字段用指针区分「未设置」与「零值」。自定义时间格式实现MarshalJSON/UnmarshalJSON,返回值必须是带引号的合法 JSON。结构不定的字段用json.RawMessage延迟解析,比map[string]any更类型安全。一句话记住:omitempty 认零值不认「你是否设置过」,布尔和数字字段用它之前先想一秒。