你好,我是徐逸。
上节课我们学习了如何使用设计模式,来提升我们代码的可维护性。不过除了设计模式之外,Go 语言本身所提供的反射和泛型特性,同样是我们手中的得力工具。借助这些特性,我们能够达成逻辑复用的目标,避免重复编写那些功能相近的函数,让代码更加简洁。
今天,我就以实现一个求最大值的函数为例,在优化这个函数实现的过程中,带你了解反射和泛型知识。
案例准备
假设我们已经有了如下的函数,用于求取两个整数间的最大值。
1 | func MaxInt(a, b int) int { |
现在,如果我们想要实现一个类似的函数,来求取两个浮点数的最大值,我们可能会像下面这样增加一个新的函数。
1 | func MaxFloat32(a, b float32) float32 { |
不过,一旦我们进一步拓展需求,需要针对int64、float64 等很多其它数值类型获取最大值,这种不断添加新函数的方式,会导致代码中出现大量逻辑极为相似的函数。这不仅会使代码库变得臃肿不堪,还会极大地增加代码维护的成本。
那么是否存在一种方法,能够让我们避免重复编写逻辑相似的函数,而是在一个统一的函数里,就可以实现对所有数值类型求最大值的功能呢?
反射:如何动态操作任意类型的对象?
在Go 1.18 版本之前,我们可以通过反射(reflection)机制,实现一个函数兼容多种不同的数据类型。反射使我们能够在程序运行时操作任意类型的对象,例如灵活地调用对象的方法和访问它的属性。
在用反射来实现支持多种类型的最大值函数之前,我们先来了解下Golang中的反射机制。
Golang通过reflect包提供了强大的反射功能,这个包的核心能力在于将 interface{} 类型的变量转换为反射类型对象 reflect.Type 和 reflect.Value。借助这两个反射类型对象,我们就可以访问和操作真实对象的方法和属性。
下面是reflect包和反射对象提供的三大核心功能。
首先是**对象类型转换功能。**正如下面的图所示,我们可以通过 TypeOf 和 ValueOf 方法将 interface{} 类型的变量转换为反射类型对象 Type 和 Value。同样,通过 Interface 方法,我们可以将 Value 对象转换回 interface{} 类型的变量。

为了让你有更直观的理解,下面我给出了一段涉及对象类型转换方法的代码,供你参考。
1 | package main |
接下来是变量值的设置功能。代码示例如下,借助 reflect.Value 对象提供的以 Set 为前缀的方法(如代码中的 SetInt 方法),我们能够对实际变量的值进行修改。同时,需要注意的是,只有当传入 ValueOf 方法的参数是变量的指针时,我们才能够通过 reflect.Value 来改变实际变量的值。
1 | package main |
最后是动态方法调用功能。如下面示例代码所示,利用 Value 对象提供的 MethodByName 或 Method 方法,我们能够获取实际对象的特定方法,随后,通过Call方法,我们可以动态地调用该方法,并传递所需的参数。
1 | package main |
在掌握了反射包 reflect 的基础知识后,我们就可以运用这个包来实现最大值函数了。下面代码是基于反射实现的这个函数。
1 | import ( |
它的核心逻辑是这样的。
首先,我们通过 reflect.ValueOf 方法,获取 Value 类型的反射对象。
紧接着,我们利用 Value 对象的 Type 方法,分别获取变量 a 和 b 的类型,并进行对比。如果两者类型不同,则抛出错误。
随后,我们使用Value对象的Kind方法来识别变量的基础数据类型。Kind方法返回一个枚举值,它涵盖了Golang中所有可能的类型,包括Bool、Int、Float64、String、Struct等。Kind方法能够揭示一个值的底层类型,即使这个值被自定义类型所包装。
例如,下面代码使用TypeOf函数获取变量的类型信息,结果显示myInt的类型是MyInt,而Kind方法获取的类型信息,结果显示的类型是int。
1 | type MyInt int |
最后,基于Kind方法返回的不同类型,我们分别调用Value对象的Int、Uint、Float等方法来获取数值变量的实际数值,并进行比较。通过这种方式,我们就实现了一个能够处理多种数值类型的最大值函数。
尽管上面借助反射,我们成功实现了支持多种数值类型的最大值函数,但反射的实现方式存在下面几个问题,我们也要特别留意。
首先是类型安全问题。当我们向这个函数传入字符串时,编译阶段无法提前察觉错误,只有在程序运行调用这个函数时才会报错,使得我们无法预先发现这类问题。你可以参考一下后面的示例代码。
1 | func TestMax(t *testing.T) { |
其次是性能问题。反射机制需要在运行时解析类型信息来执行操作,相较于直接的类型操作,这个过程会产生更高的性能开销。我们可以用下面的 Benchmark 脚本,来测试反射实现的最大值函数和普通最大值函数两者的性能差异。
1 | // MaxInt函数benchmark |
测试结果出来了,两者在性能上的差异显著。借助反射机制求取两个整型的最大值,单次操作耗时约 9.5 ns,而采用不依赖泛型的常规方式,同样的操作仅需 0.3 ns,性能差距可达 30 倍之多。
1 | killianxu@KILLIANXU-MB0 18 % go test -bench . -benchmem |
最后是代码可读性问题。在我们准备的案例中,常规的 MaxInt 函数仅需 6 行代码,简洁明了。然而,通过反射实现的 Max 函数,代码量激增到 20 多行,而且大量运用反射相关方法,这无疑极大地增加了理解难度,使得代码的可读性大打折扣。
为了避免反射的这些问题,Go在1.18版本引入了泛型特性。当我们需要根据不同类型执行差异化逻辑时,反射机制是一个不错的选择。然而,如果不同类型所对应的实现逻辑一致,那么泛型便是更优的选择。
泛型:如何实现多种类型对象的逻辑复用?
泛型允许我们在编写代码时使用类型参数,从而使代码能够适用于多种不同类型,而无需为每种类型单独编写特定的实现。在 Go 语言中,泛型主要是通过类型参数和类型约束来实现的。
以支持int和float32两种类型的最大值函数为例,我们可以通过下面的代码来实现。这里的T就是类型参数,而int | float32就是类型约束。
1 | // Max使用泛型来比较两个同类型的值(要求类型是可比较的),并返回较大的值 |
需要留意的是,Golang 的泛型机制在编译阶段,会基于传入的实际参数类型,实例化出具体的函数。以下面的代码为例,若两次调用分别传入 int 和 float32 类型,编译时就会实例化出类似 MaxInt 和 MaxFloat32 这样的函数。然而,若传入 string 类型,编译时便会报错,这样一来,我们就能提前察觉类型安全问题。
1 | var a int = 1 |
当然,上述关于泛型函数的实现与使用,仅仅是一个简易示例。实际上,为了给开发者提供更多便利,Golang 还具备更为强大的功能。
比如上面的最大值函数,如果我们希望它能支持更多类型,那么我们可以参考下面代码的做法,将类型约束放在一个单独的 interface 定义中。这样一来,就能有效避免在函数定义内出现冗长的类型约束列表,从而成功实现一个可支持多种数值类型的最大值函数。
1 | type Ordered interface { |
再比如,在实际使用过程中,我们无需显式传入类型参数。Golang 编译器具备类型推断能力,它能够依据传入的具体参数,自动推断出相应的类型参数。
1 | var a int = 1 |
那么,通过泛型实现的最大值函数,是否解决了反射存在的三个缺点呢?接下来,我们就逐一分析看看。
首先是类型安全问题。泛型在编译阶段,依据传入的具体类型进行实例化。这意味着,一旦存在类型方面的问题,在编译时便会被察觉,于是就有效规避了运行时可能出现的类型安全隐患。
接着是性能问题。泛型减少了对大量反射方法的调用,所以在性能上更具优势。我们可以借助下面的 Benchmark 脚本进行测试。
1 | // 泛型实现的最大值函数benchmark |
测试结果出来了,采用泛型实现的函数,性能与常规类型的函数大致相当,然而,相较于使用反射实现的函数,它的性能高出几十倍。
1 | killianxu@KILLIANXU-MB0 18 % go test -bench . -benchmem |
最后是可读性问题。泛型的逻辑简明直观,它规避了很多反射方法的繁杂调用,显著提升了代码的可读性。
小结
今天这节课,我以实现一个支持多种数值类型的最大值函数为例,在逐步优化它的实现的过程中,向你介绍了Golang的反射和泛型知识。
现在让我们回顾一下这节课的重点。
首先是反射机制,反射使我们能够在程序运行时操作任意类型的对象。在 Golang 里,reflect 包提供了强大的反射功能,这个包的核心能力在于将 interface{} 类型的变量转换为反射类型对象 reflect.Type和reflect.Value。借助这两个反射类型对象,我们就可以访问和操作真实对象的方法和属性。
其次是泛型功能。在 Go 语言体系中,我们能够借助类型参数与类型约束,来构建泛型类型和函数。泛型允许我们在编写代码时运用类型参数,让代码得以适配多种不同类型,无需为每种类型另行编写特定实现。
希望你好好体会反射和泛型知识,在写代码的过程中,如果需要实现逻辑复用,不妨用泛型试一试。
思考题
在实践中,哪几类场景适合用泛型来实现呢?
欢迎你把你的答案分享在评论区,也欢迎你把这节课的内容分享给需要的朋友,我们下节课再见!