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

可能是:

类型示例
int10
string“hello”
booltrue
structUser{}
slice[]int{}
mapmap[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 int

Value 保存:

{ Name:"Tom", Age:20 }

所以:

函数作用
TypeOf查看变量是什么类型
ValueOf查看变量里面有什么数据

记忆:

TypeOf → 看它是谁 ValueOf → 看它有什么

六、TypeOf 和 ValueOf 区别详解

Go 反射中最核心的两个函数:

reflect.TypeOf()reflect.ValueOf()

很多初学者第一次看到这两个函数时,会产生疑问:

为什么获取一个变量,需要两个函数?

实际上:

Go 反射把变量的信息拆成了两部分:

变量 ↓ +----------------+ ↓ Type Value 类型信息 数据信息

所以:

  • TypeOf 负责获取类型信息
  • ValueOf 负责获取数据和值信息

1. 总体区别

比较reflect.TypeOfreflect.ValueOf
作用获取变量类型获取变量值
返回类型reflect.Typereflect.Value
关注重点变量是什么类型变量里面有什么数据
主要用途分析结构操作数据
是否可以修改不能可以
常用于Name、Kind、FieldInt、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

表示:

这个类型叫:

User

4. Kind():获取类型类别

作用

Kind() 用于:

获取类型底层属于哪一种类型。

例如:

varaintt:=reflect.TypeOf(a)fmt.Println(t.Kind())

输出:

int

说明:

变量 a 的类型类别是:

int

常见 Kind 类型

Kind含义
Bool布尔类型
Int整数类型
Int88位整数
Int1616位整数
Int3232位整数
Int6464位整数
Uint无符号整数
Float3232位浮点数
Float6464位浮点数
Stringstring类型
Array数组
Slice切片
MapMap
Struct结构体
Ptr指针
Func函数
Interface接口
ChanChannel

5. Name 和 Kind 的区别

这是反射中非常容易混淆的地方。

例如:

typeStudentstruct{}s:=Student{}t:=reflect.TypeOf(s)fmt.Println(t.Name())fmt.Println(t.Kind())

输出:

Student struct

为什么两个结果不同?

因为:

Name

表示:

这个类型叫什么名字

例如:

Student

Kind

表示:

这个类型本质属于哪一类

例如:

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)

输出:

Name

Field() 返回:

reflect.StructField

StructField 常见字段

字段作用
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)

输出:

Hello

TypeOf 总结

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())

输出:

hello

6. Bool():获取布尔值

例如:

flag:=truev:=reflect.ValueOf(flag)fmt.Println(v.Bool())

输出:

true

7. Float():获取浮点值

例如:

num:=3.14v:=reflect.ValueOf(num)fmt.Println(v.Float())

输出:

3.14

8. Elem():获取指针指向的数据

Elem() 是反射中非常重要的方法。

它的作用:

获取指针指向的真实数据。

例如:

a:=10v:=reflect.ValueOf(&a)

此时:

v ↓ &a

它保存的是:

变量地址

类型:

ptr

如果想获取真正的:

a

需要:

v=v.Elem()

变化过程:

&a ↓ ValueOf() ↓ reflect.Value ↓ Elem() ↓ a

9. 为什么修改变量必须使用指针?

这是反射面试非常常问的问题。

例如:

错误方式:

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()

返回:

true

11. 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 区别?

答:

TypeOfValueOf
获取类型信息获取值信息
分析结构操作数据
不能修改可以修改

4. 为什么修改变量需要指针?

答:

因为 ValueOf 获取的是变量副本,只有传入变量地址,通过 Elem() 获取真实变量后,才能修改原变量。


5. 反射为什么慢?

答:

因为反射需要在运行时:

  • 获取类型
  • 查找字段
  • 转换数据
  • 执行操作

相比普通代码增加了额外开销。


最终记忆口诀

反射 = 运行时认识数据

TypeOf:
看类型 找结构 分析字段
ValueOf:
拿数据 改数据 调方法

一句话:

TypeOf 看“它是谁”,ValueOf 看“它有什么”。

Go 学习路线:

接口 interface ↓ 空接口 interface{} ↓ 类型断言 ↓ 反射 reflect ↓ 泛型 Generic

理解反射之前,一定要掌握接口。

因为反射本质上解决的问题就是:

接口里面装了未知类型的数据,程序如何在运行时处理它?