Go 高手(21):JSON 进阶——MarshalJSON/UnmarshalJSON、流式编码、常见坑
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 21 篇,接 信号处理与优雅关闭。入门层讲了基本的 JSON 序列化,进阶层讲自定义序列化、流式处理、常见陷阱和性能优化。
本文要回答的问题
- 怎么自定义 JSON 序列化和反序列化?
MarshalJSON接口怎么用? json.Number是什么?为什么解析 JSON 整数会变成 float64?- 流式处理大 JSON 怎么做?不用把整个 JSON 读到内存。
- 时间格式怎么处理?为什么 Go 默认用 RFC3339,后端接口常用 ISO 格式?
一、自定义序列化:MarshalJSON / UnmarshalJSON
如果你要改变某个类型的 JSON 格式,实现 json.Marshaler 和 json.Unmarshaler 接口:
type Duration time.Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d *Duration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = Duration(dur)
return nil
}用法:
type Task struct {
Name string `json:"name"`
Time Duration `json:"time"`
}
t := Task{Name: "test", Time: 5 * time.Second}
data, _ := json.Marshal(t)
// {"name":"test","time":"5s"}二、解决 float64 陷阱:json.Number
var data = []byte(`{"id": 12345678901234567890}`)
// 默认方式:解析到 any,整数变成 float64
var m map[string]any
json.Unmarshal(data, &m)
// m["id"] 是 float64,大整数可能丢失精度!
// ✅ 正确:用 json.Number
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var m map[string]any
dec.Decode(&m)
num := m["id"].(json.Number)
i64, _ := num.Int64() // 整数
f64, _ := num.Float64() // 浮点数为什么会有 float64 陷阱?因为 interface{} 无法区分整数和浮点数,JSON 规范里所有数字都是浮点数。所以如果接口返回的数字可能很大,用 UseNumber()。
三、流式处理大 JSON
如果 JSON 很大,不要用 json.Unmarshal 一次读进内存,用流式处理:
// 处理 JSON 数组里的元素,逐个处理
decoder := json.NewDecoder(reader)
// 消耗掉开头的 [
if err := decoder.Decode(&json.RawMessage{}); err != nil {
return err
}
for decoder.More() {
var item Item
if err := decoder.Decode(&item); err != nil {
return err
}
process(item) // 逐个处理,不用把所有元素都放内存
}
// 消耗掉结尾的 ]
if err := decoder.Decode(&json.RawMessage{}); err != nil {
return err
}流式编码:
writer := json.NewEncoder(output)
writer.Encode(item1) // 逐个写入
writer.Encode(item2)适合处理 GB 级的 JSON 数组,不用把整个文件读到内存。
四、时间格式自定义
Go 默认用 time.Time 的 MarshalJSON 输出 RFC3339 格式:
// 默认输出:"2006-01-02T15:04:05Z07:00"
type Event struct {
Time time.Time `json:"time"`
}如果后端接口需要 2006-01-02 15:04:05 格式,自定义:
type CustomTime time.Time
func (ct CustomTime) MarshalJSON() ([]byte, error) {
t := time.Time(ct)
s := t.Format("2006-01-02 15:04:05")
return json.Marshal(s)
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse("2006-01-02 15:04:05", s)
if err != nil {
return err
}
*ct = CustomTime(t)
return nil
}五、json.RawMessage:延迟解析
如果你有一个 JSON 对象,里面某个字段暂时不解析,只保留原始字节:
type Request struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// 先解析外层
var req Request
json.Unmarshal(data, &req)
// 根据 Type 解析 Data
switch req.Type {
case "create":
var createData CreateData
json.Unmarshal(req.Data, &createData)
case "delete":
var deleteData DeleteData
json.Unmarshal(req.Data, &deleteData)
}json.RawMessage 就是 []byte,实现了 Marshaler 和 Unmarshaler。适合动态类型场景。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 大整数解析到 interface{} 变成 float64 | 丢失精度 | 用 json.Number 或 UseNumber() |
| 零值字段被省略 | omitempty 导致字段消失 | 用指针或 *int 区分零值和缺失 |
| time.Time 格式不匹配 | 前后端格式不一致 | 自定义 MarshalJSON/UnmarshalJSON |
| 一次性读大 JSON | OOM | 流式处理,json.NewDecoder 逐个处理 |
| 嵌套结构体不实现接口 | 自定义方法不生效 | 每个嵌套类型都要单独实现 |
相关与延伸
下一篇:文件系统——io/fs、embed、path/filepath;JSON 基础,见 JSON 序列化;流式 I/O,见 io 包与接口设计。
一句话总结
Go JSON 进阶:自定义序列化实现 MarshalJSON/UnmarshalJSON;解析不确定类型的整数用 json.Number 和 UseNumber() 避免精度丢失;大 JSON 用 json.NewDecoder 流式处理;时间格式自定义就是实现序列化接口;json.RawMessage 延迟解析动态字段。