传入结构体指针后必须调用.Elem()才能安全遍历,直接传结构体变量会导致v.NumField() panic或返回0;正确做法是reflect.ValueOf(&u).Elem(),并始终检查v.IsValid()、v.Kind()==reflect.Struct及field.CanInterface()。

传入结构体指针后必须调用 .Elem() 才能安全遍历
直接传结构体变量(如 reflect.ValueOf(u))会导致 v.NumField() panic 或返回 0,因为得到的是只读副本,不可寻址。正确做法是始终传地址:reflect.ValueOf(&u).Elem()。
常见错误现象:循环里 v.Field(i) 报错 reflect: call of reflect.Value.Field on struct Value,本质是 v.CanAddr() == false。
- 必须在遍历前检查
v.IsValid()和v.Kind() == reflect.Struct - 若结构体字段本身是指针(如
Address *Address),不能直接对它调.NumField(),得先判.Kind() == reflect.Ptr再非空解引用:if !field.IsNil() { field = field.Elem() } - 对
interface{}字段,需先field.Elem()解包,再判断内部类型
嵌套结构体递归时必须手动跳过未导出字段和 nil 指针
field.CanInterface() 是唯一安全读值的守门员——它为 false 时(如小写字段 age int),调 field.Interface() 必 panic。递归入口处不加这层判断,一碰私有字段就崩。
路径拼接建议统一用 . 分隔,比如 User.Profile.Address.City,方便后续映射 JSON key 或配置项。
立即学习“go语言免费学习笔记(深入)”;
- 每次进入新层级前,先
if !field.IsValid() { continue },避免nil接口、nilslice 等导致.Len()或.MapKeys()panic - 对匿名字段(
type User struct { Profile }),fieldType.Anonymous为true,可选择展开或跳过,但不要漏掉field.CanInterface()校验 - 防循环引用不是可选项:如果结构体 A 含 B 字段,B 又含指向 A 的指针,不记录已访问地址会导致无限递归栈溢出
reflect.Type 和 reflect.Value 必须同步索引,不能混用
字段名来自 t.Field(i).Name,字段值来自 v.Field(i),二者索引必须严格一致。用 v.Type().Field(i) 拿标签(如 json:"name"),但别试图从 v.Field(i) 里反推字段名——它没这信息。
常见误用:把 v.NumField() 和 t.NumField() 当成两个独立计数器,其实它们一一对应;若中间跳过某些字段(如未导出),索引偏移就会错位。
- 遍历前固定用
t := v.Type(),然后for i := 0; i ,保证 <code>t.Field(i)和v.Field(i)对齐 - 标签提取用
t.Field(i).Tag.Get("json"),注意返回的是原始字符串字面量(不含引号),空标签返回空字符串 - 字段类型是
reflect.Struct还是reflect.Ptr,必须用field.Kind()判断,不能依赖field.Type().Name()
嵌套遍历性能差且易 panic,复杂场景优先考虑静态结构体定义
反射开销显著高于直接字段访问,每层递归都伴随多次接口断言和类型检查。一个含 3 层嵌套、20 个字段的结构体,反射遍历耗时可能是直访问的 10 倍以上。
真正难处理的不是“怎么写”,而是“什么时候不该写”:比如日志打点、通用序列化、配置绑定这类高频路径,硬上反射会拖慢关键链路;而一次性数据迁移、调试工具等低频场景才值得投入。
最容易被忽略的一点:reflect.Value 对 nil interface{}、nil map/slice 的操作极不友好,.MapKeys() 或 .Len() 前不加 IsValid() 和 Kind() 双重校验,上线后第一波流量就可能触发 panic。


















