如何在 Go 中为同时包含导出与非导出字段的结构体实现自定义 JSON 编解码

go 的 json 包默认忽略非导出(小写开头)字段,但可通过实现 marshaljson 和 unmarshaljson 方法,结合中间结构体,安全地序列化/反序列化混合可见性字段,避免递归调用栈溢出。

在 Go 中,JSON 编解码器仅能访问结构体的导出字段(即首字母大写的字段)。像 fieldA string 这样的非导出字段,默认不会参与 json.Marshal 或 json.Unmarshal。若需保留其 JSON 表现力,必须显式实现 json.Marshaler 和 json.Unmarshaler 接口——但关键在于:*绝不能在自定义方法中直接对 `Test调用json.Marshal/json.Unmarshal**,否则会触发无限递归(正如原问题中因嵌入*Test` 导致的栈溢出)。

推荐做法是定义一个纯导出、字段一一对应、仅用于 JSON 传输的中间结构体(如 TestJSON),它不包含任何业务逻辑,仅作为序列化桥梁:

type Test struct {
    fieldA string // 非导出,需手动处理
    FieldB int    // 导出,可直接访问
    FieldC string // 导出
}

// TestJSON:仅用于 JSON 编解码的导出结构体,字段名与 JSON key 对齐
type TestJSON struct {
    FieldA string `json:"fieldA"`
    FieldB int    `json:"fieldB"`
    FieldC string `json:"fieldC"`
}

func (t *Test) MarshalJSON() ([]byte, error) {
    // 将当前 Test 实例的各字段显式赋值给 TestJSON,再序列化
    return json.Marshal(TestJSON{
        FieldA: t.fieldA,
        FieldB: t.FieldB,
        FieldC: t.FieldC,
    })
}

func (t *Test) UnmarshalJSON(b []byte) error {
    var temp Tes

tJSON if err := json.Unmarshal(b, &temp); err != nil { return err } // 安全反向赋值:所有字段均为可写导出字段或本结构体内存可访问的非导出字段 t.fieldA = temp.FieldA t.FieldB = temp.FieldB t.FieldC = temp.FieldC return nil }

优势说明

  • 无递归风险:TestJSON 是独立类型,与 Test 无嵌入或指针循环依赖;
  • 可维护性强:新增字段时,只需同步更新 TestJSON 定义及两个方法中的字段映射,IDE 可辅助检测遗漏;
  • 语义清晰:分离了数据模型(Test)与序列化契约(TestJSON),符合关注点分离原则。

⚠️ 注意事项

  • 若 Test 含嵌套结构体或切片,TestJSON 中对应字段也需保持相同导出性与 JSON tag;
  • 不要尝试用 unsafe 或反射绕过导出限制——既破坏安全性,又丧失编译期检查;
  • 若非导出字段本质是内部状态(如缓存、锁、连接句柄),则不应参与 JSON 序列化,此时应重新评估设计:是否真需暴露?能否用 json:"-" 显式忽略?或改用导出字段 + json tag 控制别名?

综上,使用专用中间结构体是 Go 中处理混合可见性字段 JSON 编解码的标准、安全且可扩展的方案。