开篇:反射是什么?
我们都知道Go是静态类型语言,即如 int、float32、\[\]string,等等,变量的静态类型在编译期就已经确定;而反射不同,它能在程序运行时动态的检查、修改或操作变量类型与值。
通俗地说,就是让程序在运行过程中,能够以"照镜子(反射)"的形式看清自己变量的类型、字段、方法,甚至能动态修改值或调用这些方法。
此时我们不禁产生疑惑:为什么我们不在编译时完成这些检查和操作,而是要在运行时用反射完成?
因为**"编译时做"的前提是信息已知,而反射面对的是"未知的外部数据"和"未知的用户结构体"。**
下面看一个案例帮助理解反射的作用:
假设我们需要打印结构体的字段名和值,不用反射的话,每个结构体的字段都不一样,要单独写打印逻辑:
go
type User struct { Name string; Age int }
type Product struct { ID int; Price float64 }
// 每个类型都要重复写类似逻辑
func PrintUser(u User) { fmt.Println("Name:", u.Name, "Age:", u.Age) }
func PrintProduct(p Product) { fmt.Println("ID:", p.ID, "Price:", p.Price) }
现在因为结构体还是我们自己定义的,所以我们知道每个结构体的字段,还能通过字段重复写打印逻辑
但如果这些结构体实例是运行时才会传递进来,它对我们来说是未知的,我们也就不能编译时就写好具体逻辑
有了反射,就可以写一套通用框架来应对这样的困境:
go
import "reflect"
func PrintAny(v any) {
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
for i := 0; i < val.NumField(); i++ {
fmt.Printf("%s: %v\n", typ.Field(i).Name, val.Field(i).Interface())
}
}
// 任意结构体都能直接用
PrintAny(User{Name: "张三", Age: 20})
PrintAny(Product{ID: 101, Price: 99.9})
这就是反射的核心价值:在运行时动态探查类型、操作值,编写通用型代码。
Go 语言通过标准库中的 reflect 包来实现反射功能。
一、核心概念:Type 和 Value
反射操作主要围绕 reflect 包中的两个核心类型展开:
| 类型 | 获取方式 | 作用 |
|---|---|---|
reflect.Type |
reflect.TypeOf(x) |
描述类型元数据:名称、种类、字段、方法、标签等 |
reflect.Value |
reflect.ValueOf(x) |
封装实际值:可读、可修改、可调用方法 |
还有一个和 reflect.Type 需要做区分的类型 reflect.Kind :
它通过前面两种核心类型的点调用 Kind() 产生
-
Type是静态类型:比如type MyInt int,Type 是MyInt -
Kind是底层种类:上面例子的 Kind 是int结构体、指针、切片、map 等都属于 Kind 范畴
二、动态获取类型、字段与方法
1. 基础类型探查
go
var x int64 = 100
t := reflect.TypeOf(x)
fmt.Println(t.Name()) // int64
fmt.Println(t.Kind()) // int64
fmt.Println(t.Size()) // 8 字节
2. 结构体字段获取
go
type User struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"min=18"`
}
// 标签的标准格式是 key:"value",多个标签用空格分隔。
u := User{Name: "李四", Age: 25}
t := reflect.TypeOf(u)
// 获取字段数量
fmt.Println(t.NumField()) // 2
// 按下标获取字段信息
field := t.Field(1)
fmt.Println(field.Name) // Age
fmt.Println(field.Type) // int
fmt.Println(field.Tag) // json:"age" validate:"min=18"
// 结构体标签是反射最常用的场景之一(JSON 序列化、ORM、参数校验等都依赖它)
// Get 方法:获取标签值,不存在返回空字符串
fmt.Println(tag.Get("json")) // age
// Lookup 方法:返回值和是否存在,区分"空值"和"不存在"
v, ok := tag.Lookup("validate")
fmt.Println(v, ok) // min=18 true
Go 1.26 新增:迭代器遍历字段
Go 1.26 新增 Type.Fields() 方法,支持 for-range 直接遍历,代码更简洁:
go
// Go 1.26+ 写法
for f := range t.Fields() {
fmt.Printf("字段名: %s, 类型: %v\n", f.Name, f.Type)
}
对应 reflect.Value 也新增了 Value.Fields(),遍历同时可直接拿到字段值。
3. 方法获取与遍历
go
func (u User) SayHello() string { return "Hello, " + u.Name }
t := reflect.TypeOf(User{})
// 获取方法数量
fmt.Println(t.NumMethod()) // 1
// 按索引获取方法签名信息
method := t.Method(0)
fmt.Println(method.Name) // SayHello
fmt.Println(method.Type.NumOut) // 1
// Go 1.26+ 迭代器写法
for m := range t.Methods() {
fmt.Printf("方法名: %s, 返回值数量: %d\n", m.Name, m.Type.NumOut())
}
三、动态修改字段值
涉及到值的修改,反射 reflect.ValueOf(x) 必须传入指针,且通过 Elem () 解引用 ,否则 CanSet() 返回 false,无法修改。
为什么?因为 ValueOf 默认拿到的是值拷贝,修改拷贝没有意义;只有传入指针,才能找到原始内存地址进行修改。
再通过 CanSet() 安全检查是否可修改。
go
u := User{Name: "王五", Age: 30}
val := reflect.ValueOf(&u).Elem() // 取指针再解引用,获得可寻址的值
// 检查是否可修改
fmt.Println(val.CanSet()) // true
// 修改字段
val.FieldByName("Name").SetString("赵六")
val.FieldByName("Age").SetInt(35)
fmt.Println(u) // {赵六 35}
常用修改方法:
SetInt(x)/SetFloat(x)/SetString(x)/SetBool(x)Set(v Value):通用赋值,传入另一个 reflect.Value
四、动态调用方法
通过反射可以在运行时按名字调用方法,参数和返回值都以 []reflect.Value 传递:
go
u := User{Name: "小明"}
val := reflect.ValueOf(u)
// 按名字获取方法
method := val.MethodByName("SayHello")
// 调用方法(无参传空切片)
result := method.Call(nil)
// 取出返回值
fmt.Println(result[0].String()) // Hello, 小明
注意:方法接收者如果是指针类型,反射 reflect.ValueOf(x) 必须传入指针才能调用对应方法。
五、反射的代价
反射能力很强,但不要随便用,业务代码尽量避免。
-
性能差
反射绕开编译期类型检查,运行时做大量类型解析;比直接调用慢几十上百倍。高频路径严禁反射。
-
丢失编译期类型安全
编译不会报错,错误全部延迟到运行时 panic。参数不对、类型不匹配,运行才炸。
//编译完全没问题,运行panic
callFunc(Add,"10",2) -
代码晦涩难懂,可读性差
反射代码大量
reflect.Value、Kind、Interface(),调试麻烦。 -
不能访问未导出字段(小写)
小写私有字段反射默认读 / 写失败,需要
CanSet()判断
最后一句话总结反射的意义
静态语言编译时就要确定类型;反射给了你在运行时窥探、修改、调用未知类型的能力,用来构建通用组件,但牺牲性能和编译安全。