必须用 reflect.ValueOf(&structVar).Elem() 获取可寻址结构体反射对象,再遍历字段;需检查 v.Kind() == reflect.Struct、字段可导出(PkgPath == "")和可设置(CanSet()),解析 tag 用 structTag.Get(),避免高频反射调用并缓存类型信息。

用 reflect.ValueOf 获取结构体值再遍历字段
直接对结构体变量调用 reflect.ValueOf 得到的是其值的反射对象,但必须确保传入的是地址(&structVar),否则无法修改字段,且部分字段(如未导出字段)在非指针情况下可能无法正确访问。若传入的是值而非指针,Value.NumField() 仍可读,但后续调用 SetXxx 会 panic。
常见错误现象:panic: reflect: reflect.Value.SetXxx called on zero Value 或字段值看似“空”——实际是因传了值副本,CanAddr() 为 false,导致 Field(i).Addr() 失败。
- 始终用
reflect.ValueOf(&myStruct).Elem()开始处理,确保可寻址 - 检查
v.Kind() == reflect.Struct再继续,避免对 map/slice 等误操作 - 遍历时用
v.Type().Field(i)拿StructField(含 tag、名字),用v.Field(i)拿对应Value
判断字段是否可导出、可设置、有特定 tag
Go 反射中“可导出”不等于“可设置”,而 tag 解析完全依赖字符串切分,没有内置 schema 验证。例如 `json:"name,omitempty"` 中的 omitempty 是 json 包自己解释的,reflect 只返回原始字符串。
使用场景:做通用序列化适配、ORM 字段映射、表单绑定时,需结合 tag 和可设置性决定是否写入值。
立即学习“go语言免费学习笔记(深入)”;
- 用
field.PkgPath != ""判断是否导出(未导出字段PkgPath非空) - 用
v.Field(i).CanSet()判断能否赋值,它依赖于原始值是否可寻址(所以必须从指针.Elem()开始) - 解析 tag 推荐用
structTag.Get("json"),不要手动strings.Split;注意 tag 值可能为空或格式错误,需容错
性能敏感场景下避免高频反射调用
反射本身比直接字段访问慢 10–100 倍,尤其在循环内反复调用 v.Field(i) 或 t.Field(i)。如果结构体类型固定、字段数稳定,应缓存 reflect.Type 和字段索引信息,而不是每次重新遍历。
典型误用:在 HTTP handler 中对每个请求都执行完整反射遍历 + tag 解析。
- 将
reflect.Type和字段元数据(如目标 tag 名、是否忽略、类型转换函数)预计算并存为全局map[reflect.Type]fieldMeta - 避免在 hot path 中调用
reflect.Value.Interface(),它会触发内存分配;优先用Int()、String()等具体方法 - 如需频繁转换,考虑代码生成(
go:generate+structfield)替代运行时反射
嵌套结构体与 interface{} 字段的递归处理边界
反射遍历天然支持嵌套,但容易陷入无限递归(比如 struct 包含自身指针、或 interface{} 存了同类型实例)。同时,interface{} 字段的底层值类型未知,需先用 v.Elem() 或 v.Interface() 拆包,再判断是否为 struct。
容易踩的坑:对 nil interface 或 nil 指针字段调用 .Elem() 直接 panic;对非 struct 的 interface{} 强转 reflect.Struct 导致 panic。
- 进入递归前必须检查
v.Kind():只对reflect.Struct、reflect.Ptr(且v.IsNil() == false)、reflect.Interface(且v.IsNil() == false)做展开 - 对
reflect.Interface,先用v.Elem()获取底层值,再判断Kind();若为reflect.Ptr,需再.Elem()一次才到 struct - 建议加递归深度限制(如最大 5 层),防止意外环引用崩溃
Kind 与空值状态,比写得“通用”更重要。很多看似简洁的反射遍历代码,上线后卡在 GC 或 panic 在第 3 层嵌套里。










