Go语言反射(Reflection)详解:原理、作用、TypeOf与ValueOf区别及常用函数
Go语言反射(Reflection)详解:原理、作用、TypeOf与ValueOf区别及常用函数
一、什么是反射(Reflection)
反射(Reflection)是 Go 语言提供的一种机制:
让程序在运行期间动态获取变量的类型和值,并且可以对变量进行操作。
简单理解:
反射就是让程序在运行时能够知道“自己正在操作的数据是什么”。
通常情况下,Go 是一种静态类型语言(Static Typed Language)。
也就是说:
在程序编译阶段,变量的类型已经确定。
例如:
packagemainimport"fmt"funcmain(){varaint=10fmt.Println(a)}编译器在编译阶段已经知道:
a 是 int 类型因此可以直接进行:
- 变量存储
- 类型检查
- 方法调用
- 运算操作
但是,在实际开发过程中,经常会遇到一种特殊情况:
程序运行时,才知道数据具体是什么类型。
例如:
funcPrint(valueinterface{}){}这里的:
interface{}可以接收任意类型的数据。
例如:
Print(10)Print("hello")Print(true)此时:
value可能是:
int string bool struct slice map pointer ...但是在编译阶段,Go 并不知道:
value 到底是什么类型只有程序运行起来之后,才能确定。
那么问题来了:
如果变量的具体类型只有运行时才能知道,程序应该如何处理它?
这就是 Go 反射(Reflection)出现的原因。
二、为什么需要反射?
1. interface{} 带来的问题
Go 中的空接口:
interface{}可以保存任意类型的数据。
例如:
funcPrint(valueinterface{}){}调用:
Print(10)Print("hello")Print(true)此时:
value可能是:
| 类型 | 示例 |
|---|---|
| int | 10 |
| string | “hello” |
| bool | true |
| struct | User{} |
| slice | []int{} |
| map | map[string]int{} |
但是:
在编译阶段:
Go 不知道 value 的真实类型因此无法提前确定:
- 可以调用哪些方法
- 如何读取数据
- 如何修改数据
这就是:
静态类型语言和:
动态数据处理之间的矛盾。
2. 没有反射怎么办?
如果没有反射,只能通过:
类型断言(Type Assertion)
判断类型。
例如:
funcPrint(valueinterface{}){switchvalue.(type){caseint:fmt.Println("int")casestring:fmt.Println("string")casebool:fmt.Println("bool")}}这种方式可以解决少量类型判断。
但是如果类型越来越多:
User Order Product Student Message Config ...代码会不断增加:
caseUser:caseOrder:caseProduct:维护成本越来越高。
所以 Go 提供:
Reflection(反射)让程序可以:
运行时获取变量类型 ↓ 运行时获取变量值 ↓ 运行时修改变量 ↓ 运行时调用方法三、反射解决了什么问题?
反射主要解决:
程序在运行时不知道变量具体类型的问题。
主要有以下几个作用。
1. 运行时获取变量类型
例如:
packagemainimport("fmt""reflect")funcmain(){varaint=10t:=reflect.TypeOf(a)fmt.Println(t)}输出:
int程序运行时知道:
a 是什么类型2. 运行时获取变量值
例如:
a:=10v:=reflect.ValueOf(a)fmt.Println(v)输出:
10程序运行时可以获取:
变量里面存储的数据3. 运行时修改变量
普通情况下:
a:=10a=20必须提前知道:
a 是 int 类型但是反射可以:
不知道具体类型 ↓ 获取变量 ↓ 判断类型 ↓ 修改数据例如:
funcChange(xinterface{}){}即使不知道:
x 的真实类型也可以通过反射:
- 获取变量
- 判断类型
- 修改数据
四、反射的实际应用场景
反射在 Go 框架中非常常见。
很多我们日常使用的框架:
- Gin
- GORM
- encoding/json
底层都大量使用反射。
1. Gin 框架参数绑定
例如:
定义结构体:
typeUserstruct{NamestringAgeint}客户端发送:
{"name":"Tom","age":20}Gin 需要完成:
JSON 数据 ↓ 解析字段 ↓ 找到结构体字段 ↓ 自动赋值 ↓ 生成 User 对象但是 Gin 并不知道:
你的结构体叫什么 有哪些字段 字段是什么类型所以需要反射:
获取结构体类型 ↓ 查找字段 ↓ 修改字段值2. GORM ORM 框架
例如:
typeUserstruct{NamestringAgeint}GORM 需要知道:
| 信息 | 作用 |
|---|---|
| 结构体名称 | 对应数据库表 |
| 字段名称 | 对应数据库列 |
| 字段类型 | 决定数据转换 |
| Tag标签 | 读取配置 |
然后自动生成 SQL:
select*fromuser;这些过程依赖反射。
3. JSON 序列化
例如:
json.Marshal(user)内部需要:
读取结构体字段 ↓ 获取 Tag 标签 ↓ 转换 JSON 数据例如:
typeUserstruct{Namestring`json:"name"`}反射可以读取:
json:"name"然后完成字段映射。
五、Go 反射核心:Type 和 Value
Go 反射主要有两个核心对象:
reflect.Type reflect.Value它们分别表示:
| 对象 | 含义 |
|---|---|
| Type | 类型信息 |
| Value | 数据信息 |
可以这样理解:
假设:
user:=User{Name:"Tom",Age:20,}反射之后:
Type 保存:
User 字段: Name string Age intValue 保存:
{ Name:"Tom", Age:20 }所以:
| 函数 | 作用 |
|---|---|
| TypeOf | 查看变量是什么类型 |
| ValueOf | 查看变量里面有什么数据 |
记忆:
TypeOf → 看它是谁 ValueOf → 看它有什么六、TypeOf 和 ValueOf 区别详解
Go 反射中最核心的两个函数:
reflect.TypeOf()reflect.ValueOf()很多初学者第一次看到这两个函数时,会产生疑问:
为什么获取一个变量,需要两个函数?
实际上:
Go 反射把变量的信息拆成了两部分:
变量 ↓ +----------------+ ↓ Type Value 类型信息 数据信息所以:
- TypeOf 负责获取类型信息
- ValueOf 负责获取数据和值信息
1. 总体区别
| 比较 | reflect.TypeOf | reflect.ValueOf |
|---|---|---|
| 作用 | 获取变量类型 | 获取变量值 |
| 返回类型 | reflect.Type | reflect.Value |
| 关注重点 | 变量是什么类型 | 变量里面有什么数据 |
| 主要用途 | 分析结构 | 操作数据 |
| 是否可以修改 | 不能 | 可以 |
| 常用于 | Name、Kind、Field | Int、String、Set |
| 适合场景 | 类型判断、结构分析 | 读取数据、修改数据、调用方法 |
简单记忆:
TypeOf: 看它是谁 ValueOf: 看它有什么七、reflect.TypeOf 详解
1. TypeOf 基本使用
TypeOf 用于:
获取变量的类型信息。
示例:
packagemainimport("fmt""reflect")funcmain(){varaint=10t:=reflect.TypeOf(a)fmt.Println(t)}输出:
int返回:
reflect.Type也就是说:
reflect.TypeOf(a)得到的是:
a 的类型描述信息2. TypeOf 常用函数总结
| 函数 | 作用 | 返回值 | 使用场景 |
|---|---|---|---|
| Name() | 获取类型名称 | string | 获取具体类型名字 |
| Kind() | 获取类型类别 | reflect.Kind | 判断底层类型 |
| Size() | 获取类型占用空间 | uintptr | 查看内存大小 |
| NumField() | 获取结构体字段数量 | int | 遍历结构体 |
| Field() | 获取结构体字段信息 | StructField | 读取字段 |
| NumMethod() | 获取方法数量 | int | 分析方法 |
| Method() | 获取方法信息 | Method | 调用方法 |
| PkgPath() | 获取包路径 | string | 判断类型来源 |
3. Name():获取类型名称
作用
Name() 用于:
获取类型的具体名称。
例如:
typeUserstruct{Namestring}u:=User{}t:=reflect.TypeOf(u)fmt.Println(t.Name())输出:
User这里:
t.Name()得到:
User表示:
这个类型叫:
User4. Kind():获取类型类别
作用
Kind() 用于:
获取类型底层属于哪一种类型。
例如:
varaintt:=reflect.TypeOf(a)fmt.Println(t.Kind())输出:
int说明:
变量 a 的类型类别是:
int常见 Kind 类型
| Kind | 含义 |
|---|---|
| Bool | 布尔类型 |
| Int | 整数类型 |
| Int8 | 8位整数 |
| Int16 | 16位整数 |
| Int32 | 32位整数 |
| Int64 | 64位整数 |
| Uint | 无符号整数 |
| Float32 | 32位浮点数 |
| Float64 | 64位浮点数 |
| String | string类型 |
| Array | 数组 |
| Slice | 切片 |
| Map | Map |
| Struct | 结构体 |
| Ptr | 指针 |
| Func | 函数 |
| Interface | 接口 |
| Chan | Channel |
5. Name 和 Kind 的区别
这是反射中非常容易混淆的地方。
例如:
typeStudentstruct{}s:=Student{}t:=reflect.TypeOf(s)fmt.Println(t.Name())fmt.Println(t.Kind())输出:
Student struct为什么两个结果不同?
因为:
Name
表示:
这个类型叫什么名字例如:
StudentKind
表示:
这个类型本质属于哪一类例如:
struct特殊情况
例如:
typeMyIntint定义:
vara MyInt反射:
t:=reflect.TypeOf(a)获取 Name:
fmt.Println(t.Name())结果:
MyInt获取 Kind:
fmt.Println(t.Kind())结果:
int原因:
虽然名字叫:
MyInt但是底层本质:
int所以:
| 方法 | 代表 |
|---|---|
| Name() | 具体名字 |
| Kind() | 底层类型类别 |
记忆:
Name 看叫什么,Kind 看本质。
6. Size():获取类型占用空间
作用
Size() 用于:
获取类型在内存中占用多少字节。
例如:
varaint64t:=reflect.TypeOf(a)fmt.Println(t.Size())输出:
8表示:
int64 占用 8 字节7. NumField():获取结构体字段数量
作用
NumField() 用于:
获取结构体中字段的数量。
例如:
typeUserstruct{NamestringAgeint}u:=User{}t:=reflect.TypeOf(u)fmt.Println(t.NumField())输出:
2因为:
User 有:
Name Age两个字段。
注意:
NumField 只能用于:
Struct(结构体)例如:
intstringslice不能使用。
8. Field():获取结构体字段信息
作用
Field() 用于:
获取结构体指定位置的字段。
例如:
typeUserstruct{NamestringAgeint}u:=User{}t:=reflect.TypeOf(u)field:=t.Field(0)fmt.Println(field.Name)输出:
NameField() 返回:
reflect.StructFieldStructField 常见字段
| 字段 | 作用 |
|---|---|
| Name | 字段名称 |
| Type | 字段类型 |
| Tag | 字段标签 |
| Offset | 字段内存偏移 |
| Index | 字段索引 |
| Anonymous | 是否匿名字段 |
例如:
typeUserstruct{Namestring`json:"name"`}反射:
field.Tag可以获取:
json:"name"这也是:
- Gin
- GORM
- JSON
大量使用反射 Tag 的原因。
9. Method():获取方法信息
如果结构体拥有方法:
可以通过反射获取。
例如:
typeUserstruct{}func(User)Hello(){}获取方法:
u:=User{}t:=reflect.TypeOf(u)method:=t.Method(0)fmt.Println(method.Name)输出:
HelloTypeOf 总结
TypeOf 主要负责:
分析类型常见流程:
变量 ↓ TypeOf() ↓ Type ↓ Name() Kind() Field() Method()记忆:
TypeOf 用来看“结构”,不负责操作数据。
八、reflect.ValueOf 详解
前面学习了:
reflect.TypeOf()它主要负责:
获取类型信息但是如果想:
- 获取变量里面的数据
- 修改变量
- 调用方法
就需要:
reflect.ValueOf()1. ValueOf 基本使用
ValueOf 用于:
获取变量中的实际数据。
例如:
packagemainimport("fmt""reflect")funcmain(){a:=10v:=reflect.ValueOf(a)fmt.Println(v)}输出:
10返回:
reflect.Value也就是说:
reflect.ValueOf(a)得到的是:
a 的值信息2. ValueOf 常用函数总结
| 函数 | 作用 | 使用场景 |
|---|---|---|
| Kind() | 获取值的类型类别 | 判断数据类型 |
| Type() | 获取值的类型 | 查看具体类型 |
| Interface() | 转换回普通变量 | 恢复原始数据 |
| Int() | 获取整数值 | 读取 int 类型 |
| String() | 获取字符串值 | 读取 string 类型 |
| Bool() | 获取布尔值 | 读取 bool 类型 |
| Float() | 获取浮点值 | 读取 float 类型 |
| Elem() | 获取指针指向的数据 | 操作指针变量 |
| Set() | 修改数据 | 动态赋值 |
| SetInt() | 修改整数 | 修改 int 类型 |
| Field() | 获取结构体字段值 | 操作结构体 |
| Method() | 获取方法 | 调用方法 |
| Call() | 调用函数 | 动态执行函数 |
| CanSet() | 判断是否可以修改 | Set 前检查 |
| CanInterface() | 判断是否可以转换接口 | 安全转换 |
3. Interface():转换回普通变量
作用
Interface() 可以将:
reflect.Value转换回:
普通 Go 变量例如:
packagemainimport("fmt""reflect")funcmain(){a:=10v:=reflect.ValueOf(a)x:=v.Interface()fmt.Println(x)}输出:
10执行过程:
a ↓ ValueOf() ↓ reflect.Value ↓ Interface() ↓ 普通变量4. Int():获取整数值
作用
Int() 用于:
从反射对象中获取整数数据。
例如:
v:=reflect.ValueOf(100)fmt.Println(v.Int())输出:
100注意:
Int() 只能用于整数类型。
例如:
v:=reflect.ValueOf("hello")v.Int()会出现错误。
因为:
string 不是整数类型5. String():获取字符串
作用
String() 用于:
获取字符串数据。
例如:
v:=reflect.ValueOf("hello")fmt.Println(v.String())输出:
hello6. Bool():获取布尔值
例如:
flag:=truev:=reflect.ValueOf(flag)fmt.Println(v.Bool())输出:
true7. Float():获取浮点值
例如:
num:=3.14v:=reflect.ValueOf(num)fmt.Println(v.Float())输出:
3.148. Elem():获取指针指向的数据
Elem() 是反射中非常重要的方法。
它的作用:
获取指针指向的真实数据。
例如:
a:=10v:=reflect.ValueOf(&a)此时:
v ↓ &a它保存的是:
变量地址类型:
ptr如果想获取真正的:
a需要:
v=v.Elem()变化过程:
&a ↓ ValueOf() ↓ reflect.Value ↓ Elem() ↓ a9. 为什么修改变量必须使用指针?
这是反射面试非常常问的问题。
例如:
错误方式:
funcmain(){a:=10v:=reflect.ValueOf(a)v.SetInt(20)}为什么错误?
因为:
reflect.ValueOf(a)获取的是:
a 的副本过程:
a = 10 复制 Value = 10此时修改:
Value.SetInt(20)修改的是:
副本不是:
真正变量 a所以无法影响原变量。
正确方式:
传入地址:
funcChange(xinterface{}){v:=reflect.ValueOf(x)v=v.Elem()v.SetInt(20)}funcmain(){a:=10Change(&a)fmt.Println(a)}输出:
20流程:
&a ↓ ValueOf() ↓ Elem() ↓ 真实变量 ↓ Set()10. Set():修改变量
作用
Set 用于:
修改反射中的变量值。
例如:
v.SetInt(20)但是必须满足:
Value 可以被修改也就是:
v.CanSet()返回:
true11. CanSet():判断是否可以修改
作用:
判断当前 Value 是否允许修改。
例如:
v.CanSet()返回:
true / false一般:
reflect.ValueOf(a)不能修改。
因为:
得到的是副本而:
reflect.ValueOf(&a).Elem()可以修改。
12. CanInterface()
作用:
判断 Value 是否可以转换成 interface{}。
例如:
v.CanInterface()返回:
true表示:
可以使用:
v.Interface()九、TypeOf 和 ValueOf 到底什么时候使用?
这是学习反射最重要的一张表。
| 需求 | 使用方法 |
|---|---|
| 判断变量类型 | TypeOf |
| 判断是不是结构体 | TypeOf().Kind() |
| 获取类型名称 | TypeOf().Name() |
| 获取字段数量 | TypeOf().NumField() |
| 获取字段信息 | TypeOf().Field() |
| 获取方法数量 | TypeOf().NumMethod() |
| 获取变量数据 | ValueOf |
| 读取整数 | ValueOf().Int() |
| 读取字符串 | ValueOf().String() |
| 读取布尔 | ValueOf().Bool() |
| 获取指针数据 | ValueOf().Elem() |
| 修改变量 | ValueOf()+指针+Set() |
| 调用方法 | ValueOf().Method() |
| 调用函数 | ValueOf().Call() |
十、为什么有的方法需要括号,有的不需要?
这是 Go 中:
方法调用和字段访问的区别。
1. 方法调用需要括号
例如:
t.Name()为什么?
因为:
Name 是方法方法本质:
funcName()调用函数必须:
()例如:
fmt.Println()也是方法调用。
2. 字段访问不需要括号
例如:
field.Name这里:
Name 是结构体字段类似:
student.name字段只是读取数据:
不需要执行。
判断规则
| 形式 | 含义 |
|---|---|
| xxx() | 调用函数或方法 |
| xxx | 访问字段或变量 |
记忆:
括号代表执行,没有括号代表访问。
十一、反射完整流程
整个反射过程:
普通变量 | ↓ interface{} | ↓ +----------------+ | | TypeOf ValueOf | | ↓ ↓ 类型信息 数据信息 | | ↓ ↓ Name() Int() Kind() String() Field() Set() Method() Call()十二、反射优缺点总结
优点
1. 动态处理未知类型
可以处理:
interface{} 未知结构体 动态数据2. 提高框架通用性
例如:
- Gin
- GORM
- JSON
都依赖反射。
缺点
1. 性能较低
普通代码:
编译阶段确定类型 ↓ 直接调用反射:
运行阶段分析类型 ↓ 查找数据 ↓ 执行操作多了很多步骤。
2. 类型安全降低
普通代码:
编译器可以检查错误。
反射:
很多错误只能运行时发现。
3. 代码复杂度增加
大量使用反射:
会导致:
- 可读性降低
- 调试困难
十三、Go反射面试重点总结
1. 什么是反射?
答:
反射是 Go 在运行时动态获取变量类型和值,并可以进一步操作变量的一种机制。
2. 为什么需要反射?
答:
因为 interface{} 可以保存任意类型的数据,而程序在编译阶段不知道具体类型,需要在运行时获取类型信息。
3. TypeOf 和 ValueOf 区别?
答:
| TypeOf | ValueOf |
|---|---|
| 获取类型信息 | 获取值信息 |
| 分析结构 | 操作数据 |
| 不能修改 | 可以修改 |
4. 为什么修改变量需要指针?
答:
因为 ValueOf 获取的是变量副本,只有传入变量地址,通过 Elem() 获取真实变量后,才能修改原变量。
5. 反射为什么慢?
答:
因为反射需要在运行时:
- 获取类型
- 查找字段
- 转换数据
- 执行操作
相比普通代码增加了额外开销。
最终记忆口诀
反射 = 运行时认识数据
TypeOf:
看类型 找结构 分析字段ValueOf:
拿数据 改数据 调方法一句话:
TypeOf 看“它是谁”,ValueOf 看“它有什么”。
Go 学习路线:
接口 interface ↓ 空接口 interface{} ↓ 类型断言 ↓ 反射 reflect ↓ 泛型 Generic理解反射之前,一定要掌握接口。
因为反射本质上解决的问题就是:
接口里面装了未知类型的数据,程序如何在运行时处理它?