Go反射不提供序列化能力,实际由json.Marshal等完成;reflect.Value.Interface()仅解包值,不能直接序列化;标准序列化器内部用反射处理字段、tag和嵌套,但用户不应重复实现。

Go 的反射本身不提供序列化能力,json.Marshal 和 xml.Marshal 等标准库函数才是实际做序列化的主力;反射只是让它们能“看到”结构体字段、类型信息,从而自动处理嵌套、指针、接口等场景。
为什么不能直接用 reflect.Value.Interface() 做序列化
反射值(reflect.Value)是运行时的类型抽象,不是数据本身。调用 .Interface() 只是把反射包装解包回原始 Go 值,但这个值仍需交给真正的序列化器处理——它自己不会转成 JSON 字符串。
- 直接传
reflect.Value给json.Marshal会 panic:json: unsupported type: reflect.Value - 哪怕手动遍历
reflect.Value字段并拼字符串,也会漏掉 tag 控制(如json:"name,omitempty")、循环引用检测、时间格式化等关键逻辑 - 标准序列化器内部确实大量使用反射(比如
json.structEncoder),但这是封装好的实现细节,用户不该重复造轮子
json.Marshal 内部怎么用反射
它通过 reflect.TypeOf 和 reflect.ValueOf 获取结构体字段名、类型、tag,并递归处理每个字段。你不需要(也不应该)复现这套逻辑,但可以理解其依赖点:
- 字段必须是导出的(首字母大写),否则反射拿不到值
-
json:tag 控制键名、是否忽略空值、是否作为字符串编码等,例如Age int `json:"age,string"` - 嵌套结构体、切片、map 都会被自动递归处理,前提是元素类型也支持序列化
- 自定义类型可实现
json.Marshaler接口,此时反射会优先调用其MarshalJSON方法,跳过默认逻辑
需要反射介入的典型序列化扩展场景
当标准 json.Marshal 不够用时,才需手写反射逻辑,常见于以下情况:
立即学习“go语言免费学习笔记(深入)”;
- 动态字段过滤:运行时根据权限或配置决定哪些字段参与序列化,需用
reflect.StructField.Tag.Get("json")解析 tag 并判断是否保留 - 统一时间格式注入:遍历所有
time.Time类型字段,强制加上json:"2006-01-02"行为(需配合自定义 marshaler) - 结构体转 flat map(如用于日志打点):用反射提取所有叶子字段路径和值,生成
map[string]interface{},而非嵌套 JSON - 兼容旧版字段别名:遇到
json:"user_name"时,同时允许反序列化userName或username,需反射扫描所有可能的 tag 变体
一个安全的反射辅助序列化小工具示例
下面这个函数只做一件事:把结构体中所有非空字符串字段转成小写再序列化,其余逻辑仍交给 json.Marshal —— 这是反射和标准序列化协作的合理边界:
func MarshalWithLowercaseStrings(v interface{}) ([]byte, error) {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return json.Marshal(v)
}
// 浅拷贝,避免修改原值
newVal := reflect.New(rv.Type()).Elem()
for i := 0; i < rv.NumField(); i++ {
fv := rv.Field(i)
ft := rv.Type().Field(i)
if ft.Type.Kind() == reflect.String && !fv.IsNil() {
lowerStr := strings.ToLower(fv.String())
newVal.Field(i).SetString(lowerStr)
} else {
newVal.Field(i).Set(fv)
}
}
return json.Marshal(newVal.Interface())
}
注意:这里没处理嵌套结构体、指针字符串、interface{} 等复杂情况——那些正是标准库要负责的部分。反射只在明确需要干预的单一层级上动手。
真正容易被忽略的是字段可寻址性:如果传入的是字面量或不可寻址值(如 MarshalWithLowercaseStrings(struct{X string}{"A"})),rv.Field(i).SetString 会 panic。生产代码必须加 fv.CanInterface() 和 fv.CanSet() 判断。










