文章

从golang的反射聊起,讨论'反射'存在的必要性

从golang的反射聊起,讨论'反射'存在的必要性

1.Golang中的反射如何使用

1
golang 中的反射以reflect.ValueOf(i)和reflect.TypeOf(i)作为入口
1
2
3
4
5
6
7
func test4() {
	var s any = "zhang"
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)
	fmt.Printf("t: %v\n", t)
	fmt.Printf("v: %v\n", v)
}

输出

1
2
t: string
v: zhang

1.1.reflect.ValueOf(i)

  • reflect.ValueOf(i)返回的是reflect.Value类型,它封装了对i的运行时表示,包括它的具体值类型
  • 它不仅仅是一个具体值,而是一个可以动态访问和操作值的反射对象
  • 通过v := reflect.Value,你可以:
    • 获取变量的值(v.Int(),v.String())
    • 获取变量的类型(v.Type())
    • 调用方法(v.Method(int).Call(),v.MethodByName(string).Call())
    • 修改值(如果传入的是指针且可寻址,v.Elem().Set())
    • 获取字段值(对于结构体,v.NumField(),v.Field(j))
  • v是与具体的示例i绑定的,它持有i中的值信息和类型信息
  • 如果i是一个interface{},v会捕获接口中存储的动态值(如下)
1
2
3
4
5
6
7
8
9
10
11
func test3() {
	var s any = "zhang"
	v := reflect.ValueOf(s)
	fmt.Printf("v.Type(): %v\n", v.Type())
	fmt.Printf("v: %v\n", v)
	s = 3
	v = reflect.ValueOf(s)
	fmt.Printf("v.Type(): %v\n", v.Type())
	fmt.Printf("v: %v\n", v)
}

输出

1
2
3
4
v.Type(): string
v: zhang
v.Type(): int
v: 3

1.2.reflect.TypeOf(i)

  • reflect.TypeOf(i)返回的是reflect.Type类型,表示i的类型的元数据
  • 只包含类型信息,不包含任何具体的值
  • 通过t := reflect.Type,你可以:
    • 获取类型名称(t.Name())
    • 获取底层类型类别(t.Kind())
    • 检查字段(对于结构体,例如t.NumField(),需要注意的是,这里只有字段的类型,没有字段的值)
    • 检查方法(例如t.NumMethod(),t.Method())
  • t是与i的类型绑定的,但它不持有i的具体值
  • 它是一个静态的、抽象的表述,独立于任何示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func test1() {
	user := &User{
		Name: "zhang",
		Age:  26,
	}
	user1 := &User{
		Name: "zhang3",
		Age:  27,
	}
	v := reflect.ValueOf(user) //v
	t := reflect.TypeOf(user) //t
	fmt.Printf("v: %v\n", v)
	fmt.Printf("t: %v\n", t)
	v.Method(0).Call([]reflect.Value{reflect.ValueOf("world")}) //reflect.Value本来就已经与实例绑定,可以直接调用方法
	v1 := reflect.ValueOf(user1)
	t.Method(0).Func.Call([]reflect.Value{v1, reflect.ValueOf("world")}) //reflect.Type没有与任何实例绑定,因此在调用方法时需要手动提供接收者v1
}

输出

1
2
3
4
v: &{zhang 26 {}}
t: *main.User
hello zhang , world
hello zhang3 , world

1.3.类比理解

  • reflect.ValueOf(i): 像是一个装着具体物品(值)的盒子,你可以打开盒子取出物品(值),也可以看到盒子上写的标签(类型)。 例子:一个装着“42”的盒子,标签写着“int”。
  • reflect.TypeOf(i): 像是一个物品的说明书,只告诉你物品的规格(类型信息),但手里没有实际的物品。 例子:一份“int 类型说明书”,告诉你这是整数类型,但没有具体的“42”。

2.反射的存在,有什么必要性

2.1.反射的核心价值

反射的核心价值在于运行时的动态性,对于一些框架和一些通用处理来说,有些需求需要在运行时处理未知或者动态的数据类型,而反射恰恰填补了这些空白

2.2.如果没有反射,哪些功能会变得难以实现

  • 序列化与反序列化
1
2
3
4
5
6
7
json.Marshal()与json.Unmarshal()中,使用反射获取了结构体中的tags,并将其映射到json数据中。

开发者只需定义结构体和标签,即可实现序列化与反序列化

如果没有反射,你就需要为每种结构体手动编写Marshal()和Unmarshal()函数,逐个字段处理。

并且,你也无法编写通用的序列化库,必须针对每种类型写特定实现,违背DRY(Don't Repeat Yourself)原则
  • 对象关系映射(ORM)
1
2
3
4
5
ORM框架通过反射分析结构体字段,将其映射到数据库表字段,反射动态生成SQL。

也可以通过反射构造建表语句。

如果没有反射,你需要为每个模型手动编写CRUD操作的SQL语句;你也无法实现通用的数据库操作函数,每次新增模型或修改字段都需要重写代码
  • 依赖注入
1
2
3
依赖注入框架通过反射动态分析类型间的依赖关系,在运行时构造对象

如果没有反射,你需要手动注入依赖关系,对于大型项目来说,依赖关系维护很繁琐也很容易出错
  • 动态调用方法
1
通过反射可以动态调用方法,即使在编译时不知道具体类型,这在插件系统和RPC系统中非常有用
  • 处理接口类型(interface{})的动态行为
1
2
3
golang中interface{}可以持有任意类型,反射是处理这种动态类型的标准方式。

如果没有反射,你只有通过类型断言逐一尝试已知类型,无法处理未知类型
  • 运行时配置与插件系统
1
反射支持根据运行时配置动态加载和调用代码。例如,Web框架(如gin)通过反射解析路由和处理函数

3.反射的代价与权衡

尽管反射很有必要,但它也有缺点

  • 性能开销:反射比直接调用慢,因为设计运行时检查和动态分配
  • 代码安全性:反射无法通过编译器检查代码,容易引发运行时错误
  • 复杂性:反射在代码可读性上和维护上都比较差

因此,反射通常用于初始化阶段和低频操作,而不是高性能路径


4.总结:反射的必要性

反射的存在必要性在于它提供了运行时的灵活性和通用型,解决了静态语言在面对动态需求时的局限

如果没有反射:

  • 通用库(JSON,ORM)难以实现
  • 动态行为(插件、方法调用)难以实现

因此,我们可以在一些动态需求的实现上考虑使用反射。但是在需要高性能的场景上,应尽量避免反射的使用

本文由作者按照 CC BY 4.0 进行授权

热门标签