Go 的 json.Marshal 本质依赖反射实现:通过 reflect.ValueOf 获取字段信息,仅导出字段可被访问,标签控制键名或忽略,未导出字段无论有无标签均被跳过;实现 MarshalJSON 方法可绕过反射提升性能。
json.Marshal 本质就是靠反射工作的你写的 json.Marshal(obj) 看似简单,但它内部会调用 reflect.ValueOf(obj) 获取结构体字段的类型、标签、值等信息。没有反射,标准库就无法自动遍历结构体字段、读取 json: 标签、跳过未导出字段——这些都不是编译期能确定的事。
这意味着:只要用了 encoding/json,你就已经在用反射了;想绕开它,只能手写序列化逻辑或换用不依赖反射的库(如 easyjson 或 ffjson)。
json.Marshal 对字段可见性的处理完全由反射控制Go 反射只能访问导出(首字母大写)字段,json.Marshal 正是基于这一点实现“默认忽略私有字段”。但注意,这和“是否带 json: 标签”无关——即使给私有字段加了 json:"name",它依然不会被序列化,因为反射根本读不到它的 reflect.StructField。
json: 标签 → 使用字段名小写形式作为 keyjson:"foo" → 使用 "foo" 作为 keyjson:"-" → 被忽略(反射能读到,但 json 包显式跳过)name string)→ 反射无法获取,直接跳过,加任何标签都无效MarshalJSON 方法会绕过反射字段遍历当类型实现了 MarshalJSON() ([]byte, error),json.Marshal 会直接调用它,不再走反射路径。这是性能优化的关键出口,也是控制序列化行为最彻底的方式。
立即学习“go语言免费学习笔记(深入)”;
常见使用场景:
PasswordHash 写进 JSON)CreatedAt time.Time 转成 Unix 时间戳整数)func (u User) MarshalJSON() ([]byte, error) {
type Alias User // 防止无限递归
return json.Marshal(&struct {
*Alias
CreatedAt int64 `json:"created_at"`
}{
Alias: (*Alias)(&u),
CreatedAt: u.CreatedAt.Unix(),
})
}
每次 json.Marshal 都要动态检查结构体布局、解析标签、分配临时 map/slice——这些操作无法被编译器优化,且会触发内存分配。压测中常见现象:
map[string]interface{} 类型)reflect.Value.Field 和 strings.Split(解析 tag)上如果服务每秒处理上万次 JSON 序列化,且结构体稳定,建议提前用 go:generate 工具(如 easyjson)生成无反射的 MarshalJSON 实现——它把反射过程移到了编译期。