因为json tag仅控制字段名、忽略或省略空值等反射级行为,无法改变序列化逻辑,如time.time格式转换或类型映射;必须实现json.marshaler接口并用类型别名避免递归,才能接管字节流生成。

为什么直接给 struct 加 json tag 不起作用
因为 json tag 只控制字段名、是否忽略、是否省略空值等反射级行为,它不改变字段本身的序列化逻辑。比如你想把 time.Time 输出为 "2006-01-02" 而不是 RFC3339 字符串,加 json:"created,format=2006-01-02" 完全无效——encoding/json 根本不识别 format 这个 tag key。
常见错误现象包括:
- 时间字段始终输出带时区的长字符串,改 tag 没反应
- 密码字段设成
json:"-"是隐藏了,但业务层仍要手动脱敏再赋值,污染数据 - 枚举类型是
int,想序列化成字符串(如1 → "active"),tag 无法做类型转换
根本原因是:tag 是描述性的,而 MarshalJSON() 是执行性的。只有实现接口,才能接管整个字节流生成过程。
如何正确实现 MarshalJSON 方法避免无限递归
最常踩的坑是:在 MarshalJSON() 里直接调用 json.Marshal(&s),导致反复触发自身方法,最终栈溢出 panic。
安全写法必须切断递归链,标准解法是用类型别名绕过:
- 定义
type Alias MyStruct(注意不是指针) - 在方法内构造匿名结构体或使用该别名,显式跳过当前类型的
MarshalJSON - 接收者用值类型还是指针,取决于你是否需要修改原字段;但别名类型本身不能是指针
示例片段:
func (s MyStruct) MarshalJSON() ([]byte, error) {
type Alias MyStruct // 别名不实现 MarshalJSON
return json.Marshal(struct {
*Alias
FormattedTime string `json:"created"`
}{
Alias: (*Alias)(&s),
FormattedTime: s.Created.Format("2006-01-02"),
})
}
UnmarshalJSON 的三个硬性检查点
UnmarshalJSON([]byte) 容易写出不可靠代码,尤其在框架中被自动调用时。必须确认以下三点:
- 接收者必须是指针(
func (s *MyStruct) UnmarshalJSON(...)),否则无法修改原结构体字段 - 内部不能调用
json.Unmarshal(data, s)—— 同样会递归;应先解析为map[string]json.RawMessage或临时 struct - 字段名校验要严格:比如期望字段叫
"url",但输入是"URL",就得提前处理大小写或返回明确错误,不能静默忽略
性能提示:用 json.RawMessage 延迟解析嵌套字段,能显著减少中间对象分配,适合大 payload 场景。
框架里你的 MarshalJSON 为什么没被调用
Gin/Echo/Fiber 等框架只在最终响应体类型上检查接口实现,不会穿透包装层。失效场景比想象中多:
- 返回的是
*MyStruct,但MarshalJSON实现的是值接收者func (s MyStruct) MarshalJSON()—— 指针类型不满足该方法集 - 结构体字段嵌了自定义类型,但外层没加
json:",inline",导致内层方法根本没机会执行 - 用了响应包装器如
Response{Data: user},而Response没实现Marshaler,框架只会对它做默认序列化
验证方式很简单:在 MarshalJSON 开头加一行 log.Println("called"),看是否真被触发。没日志,说明类型没被框架“看到”,而不是逻辑写错了。
真正容易被忽略的是:一旦用了自定义序列化,你就得自己负责所有字段的 JSON 安全性,比如字符串拼接时未转义引号,会导致输出非法 JSON。别图省事手拼 "{"error":" + msg + "}",必须走 json.Marshal 处理子字段。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











