不能直接用 json.Marshal 做通用序列化抽象,因其对 nil slice、零值字段、时间格式、tag 处理逻辑无法复用于 protobuf/yaml;且各格式错误行为不一(如 yaml 解码类型不匹配会 panic),需统一错误分类与上下文控制。

为什么不能直接用 json.Marshal 做通用序列化抽象
因为不同场景对序列化行为要求差异极大:微服务间通信可能需要紧凑的 protobuf,日志写入可能倾向可读的 json,而配置加载又常依赖 yaml;更关键的是,json.Marshal 对 nil slice、零值字段、时间格式、字段标签(如 json:"-" )的处理逻辑无法统一复用到其他格式。硬套一个接口包装所有 Marshal/Unmarshal 函数,反而会让错误传播变隐晦——比如 yaml.Unmarshal 遇到类型不匹配时 panic,而 json 只返回 error。
定义接口时必须包含上下文与错误分类
抽象层的核心不是“支持多种格式”,而是“让调用方能预判失败原因并可控降级”。因此接口不能只写:
type Serializer interface {<br> Marshal(v interface{}) ([]byte, error)<br> Unmarshal(data []byte, v interface{}) error<br>}而应显式区分:编码失败、解码失败、数据损坏、不支持类型。实际建议这样设计:type SerializeError struct {<br> Kind SerializeKind // 例如 SyntaxError, TypeError, UnsupportedValue<br> Err error<br>}<br><br>type Serializer interface {<br> Marshal(ctx context.Context, v interface{}) ([]byte, error)<br> Unmarshal(ctx context.Context, data []byte, v interface{}) error<br>}
-
ctx支持超时和取消,尤其在反序列化大 payload 或网络流时避免卡死 - 所有实现必须将底层 error 包装为
SerializeError,禁止裸抛fmt.Errorf或第三方库原生 error -
Unmarshal必须拒绝空data和nil目标指针,且提前校验目标是否为指针类型,否则 runtime panic
protobuf 实现最容易踩的三个坑
Go 的 protobuf(google.golang.org/protobuf)不是“拿来即用”的序列化器,它强制要求结构体实现 proto.Message 接口,且字段必须带 protobuf tag。直接拿普通 struct 去 Marshal 会 panic:
- 错误写法:
proto.Marshal(&MyStruct{})→ panic: "message is not a proto.Message" - 正确路径:用
protoc-gen-go生成 .pb.go 文件,或手动实现XXX_*方法(不推荐) - 时间字段必须用
google.protobuf.Timestamp,不能用time.Time;需配合ptypes.TimestampProto转换 - 如果想兼容非 proto struct,得加一层适配器:把输入 struct 映射到临时 proto message,再 marshal —— 但性能损耗明显,仅限低频配置场景
如何让 json/yaml/protobuf 共享同一套字段控制逻辑
不同序列化器对字段忽略、重命名、零值处理的 tag 名完全不同:json:"name,omitempty"、yaml:"name,omitempty"、protobuf:"name,opts=nullable"。硬编码多套 tag 维护成本高。可行方案是引入中间描述符:
立即学习“go语言免费学习笔记(深入)”;
type FieldRule struct {<br> Name string<br> OmitEmpty bool<br> Rename map[string]string // format → tag name<br>}
然后在各 Serializer 实现里按需生成对应 tag 字符串,或用反射动态设置 struct field tag(注意:Go 中无法在运行时修改已定义 struct 的 tag,所以该方案仅适用于构建时生成代码,或使用 map[string]interface{} + 手动键值映射)。
真正轻量且实用的做法是:放弃 tag 统一,转而用封装函数做字段过滤。例如定义 FilterFields(v interface{}, keep func(string) bool) interface{},在 Marshal 前先做一次字段裁剪,这样 json/yaml/protobuf 都能走同一套逻辑,且不依赖底层 tag 解析。
抽象层的价值不在“看起来统一”,而在“出问题时知道从哪查”。字段控制逻辑分散在 tag 里,排查时就得翻三份文档;收拢到代码里,一眼就能看到哪些字段被跳过、为什么跳过。


















