第四步的核心就是实现 json.Marshaler / json.Unmarshaler 两个接口:MarshalJSON() ([]byte, error) 和 UnmarshalJSON([]byte) error。关键是内部用类型别名去掉方法集,否则会无限递归。

type Timestamp time.Time

// 值接收者:保证 map 值、interface 里的值也能走到自定义逻辑
func (t Timestamp) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(t).Format("2006-01-02"))
}

// 解析必须用指针接收者,否则改不到原值
func (t *Timestamp) UnmarshalJSON(b []byte) error {
    if string(b) == "null" { return nil }   // null 要单独兜
    var s string
    if err := json.Unmarshal(b, &s); err != nil { return err }
    parsed, err := time.Parse("2006-01-02", s)
    if err != nil { return err }
    *t = Timestamp(parsed)
    return nil
}

若要复用内置逻辑,先做一次 type plain Timestamp 转换,再 json.Marshal(plain(t)),这样方法集被剥离,不会递归。

帖子正文有几处值得补充。第一,MarshalJSON 和 UnmarshalJSON 的接收者类型不对等:序列化时若方法定义在指针上,而值不可寻址(map 元素、interface 装箱值),自定义逻辑会被跳过而不报错,所以序列化侧用值接收者更稳。第二,帖子说“只想序列化不输出、解析仍能读入”要换写法,这里更干净的做法是拆成入库结构体和出参 DTO 两个类型,用一个转换函数处理,比在标签上纠结更清晰。第三,json:"-" 和 json:"-," 不是一回事,后者表示字段名就叫 -。

另外 ,string 选项,官方文档明确列的只有 string、浮点、整型、布尔四类,TextMarshaler 那部分我印象里不在其中,建议再核对下文档。

最常踩的坑是 MarshalJSON 返回非法 JSON(比如手拼字符串漏转义导致 quote 冲突),整个编码会直接失败,别手拼,统一走 json.Marshal。