直接用 json.marshal/unmarshal 不可靠,因会丢失字段名、忽略非导出字段、转换 time.time 为字符串甚至 panic;应使用 reflect 递归遍历,保留路径、跳过非法类型、防循环引用,并支持 tag 覆盖与静默容错。

为什么直接用 json.Marshal 再 json.Unmarshal 到 map[string]interface{} 不可靠
因为结构体字段可能带 json: tag(比如 json:"user_id"),也可能有嵌套、指针、非导出字段、时间类型、自定义 marshaler 等。直接走 JSON 流程会丢失原始字段名、忽略未导出字段、把 time.Time 变成字符串、甚至 panic(如含 func 或 chan 类型)。这不是“扁平化”,是“序列化后失真”。
用 reflect 递归遍历字段时,怎么保留原始字段路径并避免无限循环
核心是用字段名拼接路径(如 User.Profile.Name),而非只取最内层键名;同时必须跳过非导出字段、函数、通道、不支持的类型,并对指针/接口做解引用。还要记录已访问的地址,防止循环引用(比如结构体 A 包含指向自身的指针)。
- 用
reflect.Value的Kind()判断类型:遇到ptr先Elem(),遇到interface{}先Value.Elem()(如果非 nil) - 用
map[uintptr]bool记录已访问的指针地址,每次进入指针前检查value.CanAddr() && value.Addr().Pointer() - 对
time.Time、int64等基础类型直接取值;对struct递归处理;对map和slice建议跳过或按需展开(扁平化通常不展开容器)
flattenStruct 函数该怎么设计签名和边界行为
输入应为 interface{},但内部只处理 struct 类型;输出用 map[string]interface{} 最灵活。关键要定义清楚“扁平”的语义:是否包含嵌套字段?是否跳过零值?是否支持 tag 覆盖?
- 推荐默认不跳过零值(
0、空字符串、nilslice 都保留),否则业务逻辑难预测 - 支持通过参数传入自定义 tag key(如
"flat"),优先读该 tag,fallback 到字段名 - 遇到无法处理的类型(如
func、unsafe.Pointer)应静默跳过,而不是 panic —— 这是工具函数,不是校验器 - 示例调用:
result := flattenStruct(user, "json") // 用 json tag 作 key
实际使用中容易被忽略的三个细节
一是嵌套结构体字段名冲突:如果 A.B.Name 和 A.Name 同时存在,后者会被前者覆盖 —— 扁平化天然不保证唯一性,得靠使用者命名规范或加前缀;二是 interface{} 字段若存的是 struct,不会自动展开,得在递归里显式判断 value.Kind() == reflect.Interface 并 value.Elem();三是 reflect.ValueOf(nil) 会 panic,所有解引用前必须 value.IsValid() && !value.IsNil()。
真正麻烦的不是写反射,而是决定“哪些该扁平、哪些该停住”。比如要不要展开 sql.NullString?要不要把 url.URL 变成 map?这些没标准答案,得根据下游消费方约定来切。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











