本节摘要:反射让程序在运行时检视与操作类型信息——reflect 包把任意值包装成镜像对象,可查类型与种类、遍历结构体字段并读写、动态调用方法。本节覆盖两个入口函数、Type 与 Value 的分工、结构体字段操作与方法调用的实战、典型应用场景(序列化、ORM、依赖注入),以及性能与可读性的代价清单。
阅读完本节,你应当能够:
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 分步验证;错误信息里带"反射调用"字样时,先查参数镜像切片的类型是否与签名严格一致。
下一节把镜子换成手术刀:unsafe 包。