跳到主要内容

Go 里 struct tag、JSON 编解码的几个稳妥习惯

· 阅读需 4 分钟
一介布衣
全栈开发者 / 技术写作者

Go 标准库里的 encoding/json 已经够用了,平时做接口、配置读取、消息体解析,大多数场景都离不开它。问题在于,很多初学阶段写出来的 JSON 代码“能跑”但并不稳。真正进入接口联调或者服务之间互相调用时,字段命名、默认值、空值含义这些细节很快就会冒出来。

先把“内部结构”和“对外结构”分开

我比较不建议把数据库模型、业务对象、接口返回结构长期混成一个 struct。刚开始项目小,看起来省事,但字段一多,tag 会越来越混乱,后来你会发现改一个字段名要影响三层逻辑。

更稳的做法是:接口输入、接口输出、内部业务对象各自有自己的结构。哪怕字段大体相似,也不要怕多写一次转换。Go 的 struct 本来就鼓励清晰边界,别为了省几行代码,把后面的维护成本提前种下。

tag 命名别偷懒,和接口文档保持一致

字段名一旦对外暴露,就不是你本地随便起个驼峰就完事了。接口层最好明确规定 JSON 字段名,然后在 tag 里写死,而不是依赖默认转换。这样做最大的价值不是“规范”,而是避免前后端、不同服务、不同语言之间产生猜测。

type UserProfile struct {
ID int64 `json:"id"`
NickName string `json:"nick_name"`
AvatarURL string `json:"avatar_url"`
}

看起来只是多写一点点,但以后别人读结构体时,不需要再脑补最终传输层长什么样。

omitempty 很方便,但别把语义也一起省掉

很多人喜欢给所有字段都加 omitempty,结果返回值里字段忽有忽无,调用方很难判断到底是“没有值”还是“这个字段本来就不存在”。这个习惯短期看起来省流量,长期却容易把接口语义搞模糊。

我现在更倾向于只在真正允许缺席的字段上使用它。特别是那些带状态含义、计数含义、布尔含义的字段,如果轻易省掉,排查问题时会很痛苦。与其让调用方猜,不如明确给出零值。

反序列化以后,别忘了补业务层校验

json.Unmarshal 成功,不代表业务一定合法。它只能说明这段 JSON 能顺利映射到你的结构体,不代表字段组合满足业务要求。比如字符串可以为空、数字可以是零、切片可以是空数组,这些在语法上都没问题,但在业务上可能完全不成立。

所以我比较喜欢在解析完成后立刻接一个校验函数,把“结构层解析成功”和“业务层数据有效”拆开。这样后面排查错误时,你能很清楚知道问题是出在请求格式,还是出在业务约束。

对默认值要有意识,不要等到线上再补

Go 的零值机制很方便,但也容易让人误以为“不报错就是合理”。实际上很多配置型 JSON、任务参数 JSON 都需要明确默认值策略。哪些字段缺失后走默认,哪些字段缺失就应该报错,这些最好在结构体旁边就想清楚。

如果默认值逻辑散落在多个调用点里,后面一改需求就会非常混乱。把默认值补齐放在一个统一的小函数里,通常会比在各处临时判断稳很多。

JSON 用得越多,越要珍惜结构边界

Go 写 JSON 最容易让人产生错觉:标准库简单,似乎很多细节都不用管。但真正影响代码寿命的,从来不是 MarshalUnmarshal 这两个调用本身,而是你是否把字段语义、默认值策略、内外结构边界提前定义清楚。

只要这几件事情做稳,标准库其实已经足够支撑绝大多数日常接口场景,而且维护起来也会轻很多。