反射是什么?

开篇:反射是什么?

我们都知道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) 必须传入指针才能调用对应方法。

五、反射的代价

反射能力很强,但不要随便用,业务代码尽量避免

  1. 性能差

    反射绕开编译期类型检查,运行时做大量类型解析;比直接调用慢几十上百倍。高频路径严禁反射。

  2. 丢失编译期类型安全

    编译不会报错,错误全部延迟到运行时 panic。参数不对、类型不匹配,运行才炸。

    //编译完全没问题,运行panic
    callFunc(Add,"10",2)

  3. 代码晦涩难懂,可读性差

    反射代码大量reflect.Value、Kind、Interface(),调试麻烦。

  4. 不能访问未导出字段(小写)

    小写私有字段反射默认读 / 写失败,需要CanSet()判断

最后一句话总结反射的意义

静态语言编译时就要确定类型;反射给了你在运行时窥探、修改、调用未知类型的能力,用来构建通用组件,但牺牲性能和编译安全。

相关推荐
北冥you鱼1 天前
Go语言大数(big.Int)比较大小:原理、方法与最佳实践
开发语言·后端·golang
xcLeigh1 天前
Go入门:零值与变量默认初始化机制
开发语言·后端·golang
DLYSB_1 天前
数据库连接池爆满导致全线宕机:我用 Go 写了个“现场声光报警器”,比钉钉快了 10 倍
数据库·golang·钉钉·报警灯
Awna2 天前
Golang 大小写可见性规范
开发语言·后端·golang
进击的程序猿~2 天前
Go 内存分配与垃圾回收源码深度学习手册
开发语言·后端·golang
geovindu2 天前
go:Bit Operation Algorithm
开发语言·后端·算法·golang·位运算法
北冥you鱼3 天前
Go语言四则运算实战:从基础类型到big包的深度解析
开发语言·后端·golang