6.1 反射:运行时的类型镜像


6.1 反射:运行时的类型镜像

本节摘要:反射让程序在运行时检视与操作类型信息——reflect 包把任意值包装成镜像对象,可查类型与种类、遍历结构体字段并读写、动态调用方法。本节覆盖两个入口函数、Type 与 Value 的分工、结构体字段操作与方法调用的实战、典型应用场景(序列化、ORM、依赖注入),以及性能与可读性的代价清单。

读前必看(上)

阅读完本节,你应当能够:

  1. 用两个入口函数把值变成镜像对象并解释 Interface 方法的回流;
  2. 区分 Type(类型)与 Kind(底层种类);
  3. 用反射遍历并修改结构体字段(含嵌套);
  4. 动态按名调用方法;
  5. 列举反射的三大代价与"该不该用"的判断标准。

一、两个入口,一面镜子

var x float64 = 3.14 t := reflect.TypeOf(x) // 类型镜像:float64 v := reflect.ValueOf(x) // 值镜像:3.14

TypeOf 与 ValueOf 都接收空接口参数——任何值塞进 any 都能照镜子。想从镜像取回原值,用 Value 的 Interface 方法再断言回来:y := v.Interface().(float64)

先分清两个容易混的词:Type 是声明的类型(如自定义的 User),Kind 是底层的机械种类(struct、int、slice、map)。reflect.TypeOf(User{}).Kind() 是 struct,而 Type 的 String 才是 User。做分支判断通常按 Kind,报错展示用 Type。

二、改值的前提:地址与可设置性

第 3 章讲过"一切传值",这里吃个回旋镖:ValueOf 接收的是值的拷贝,镜像是只读的。想通过反射改原值,必须传入指针再取元素:

u := User{Name: "Ada", Age: 36} v := reflect.ValueOf(&u).Elem() // 取指针指向的元素,获得可写视图 nameField := v.FieldByName("Name") fmt.Println(nameField.CanSet()) // true nameField.SetString("Grace") // 原件被修改

CanSet 报告可写性,规则追根究底还是"是否从指针走来"。导出与否同样有效:小写字段 CanSet 为假,反射也翻不出可见性的墙。

三、遍历结构体:序列化的心脏

type User struct { Name string `json:"name"` Age int `json:"age"` } func dumpFields(v any) { rv := reflect.ValueOf(v) rt := reflect.TypeOf(v) for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) fv := rv.Field(i) fmt.Printf("%s 类型=%s 值=%v 标签=%s\n", f.Name, f.Type, fv.Interface(), f.Tag.Get("json")) } }

NumField 数字段、Field 按下标取。结构体标签(反引号里的键值)就藏在字段元信息里,Tag 的 Get 方法按键取值——标准库的 JSON 编解码就是靠这个循环加标签驱动的。嵌套结构体递归进去即可;Kind 为指针时先解引用再继续。

四、动态调用方法

type Calc struct{} func (c Calc) Add(a, b int) int { return a + b } c := Calc{} m := reflect.ValueOf(c).MethodByName("Add") args := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)} result := m.Call(args) // 返回[]reflect.Value fmt.Println(result[0].Int()) // 5

Call 的参数与返回值都是镜像切片。方法必须导出(小写方法照不见),参数数量类型必须严丝合缝,否则 panic。指针接收者的方法要用指针的镜像去找——又见第 3 章方法集规则。

图:反射镜像的操作面

图:反射镜像的操作面

五、应用场景:框架的专利

反射在业务代码里几乎不该出现,它的地盘在通用框架与库:JSON、XML、YAML 等编解码器靠字段遍历加标签;ORM 把数据库行映射进结构体;依赖注入容器按类型装配;测试断言库做深度比较。共同特征:调用方不知道你的类型,而你又必须处理所有类型——只有这个矛盾存在时,反射才是正当的。

判断 结论
有没有编译期已知的类型写法 有 → 别用反射
库是否必须处理任意用户类型 是 → 反射正当
热点路径是否高频 是 → 换代码生成或泛型
Go 1.18 后有没有泛型替代 常常有 → 先试泛型

⚠️ 常见坑:反射改值忘了从指针进来,CanSet 是假还硬 Set,直接 panic。改值三步走:传指针、取 Elem、查 CanSet。

💡 关键直觉:反射是 X 光机——体检时照一照看清内部(框架层),没人拿它当日常照明(业务层),辐射(性能与类型安全损失)不小。

六、反射能力边界图

问答补遗。反射能创建新类型吗? 基本不能——Go 没有运行时定义新类型的能力,只能构造已有类型的实例(动态建切片、map 等)。反射与泛型怎么配合? 泛型在编译期解决"类型参数化",反射解决"运行时未知类型",前者覆盖的场景优先前者;两者叠加常见于通用容器库里层用反射兜底。反射代码怎么调试? 打印 Type 与 Kind 分步验证;错误信息里带"反射调用"字样时,先查参数镜像切片的类型是否与签名严格一致。

要点速记

  • 两个入口:TypeOf 照类型、ValueOf 照值;Interface 方法从镜像回流原值。
  • Type 对 Kind:声明类型对底层种类,分支按 Kind、展示按 Type。
  • 可写性链条:传指针、取 Elem、CanSet 为真才许改;小写字段绝缘。
  • 字段遍历加标签是序列化与 ORM 的实现心脏。
  • Method 加 Call 动态调方法,只照得见导出方法。
  • 地盘在框架:处理"未知的任意类型"才轮到反射。
  • 三大代价:慢、失去编译期检查、工具链失明;能用泛型先泛型。

下一节把镜子换成手术刀:unsafe 包。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U