今天来聊一个平时用的不多,但是很多框架或者基础库会用到的语言特性–反射,反射并不是Go
语言独有的能力,其他编程语言都有。这篇文章的目标是简单地给大家梳理一下反射的应用场景和使用方法。
我们平时写代码能接触到与反射联系比较紧密的一个东西是结构体字段的标签,这个我准备放在后面的文章再梳理。
我准备通过用反射搞一个通用的SQL
构造器的例子,带大家掌握反射这个知识点。这个是看了国外一个博主写的例子,觉得思路很好,我又对其进行了改进,让构造器的实现更丰富了些。
本文的思路参考自:golangbot.com/reflection/ ,本文内容并非只是对原文的简单翻译,具体看下面的内容吧~!
文章内容已收录到《Go开发参考书》 这个仓库里,目前已经收集了70多条开发实践。
什么是反射
反射是程序在运行时检查其变量和值并找到它们类型的能力。听起来比较笼统,接下来我通过文章的例子一步步带你认识反射。
为什么需要反射
当学习反射的时候,每个人首先会想到的问题都是 “为什么我们要在运行时检查变量的类型呢,程序里的变量在定义的时候我们不都已经给他们指定好类型了吗?” 确实是这样的,但也并非总是如此,看到这你可能心里会想,大哥,你在说什么呢,em… 还是先写一个简单的程序,解释一下。
1 | go复制代码package main |
在上面的程序里, 变量i
的类型在编译时是已知的,我们在下一行打印了它的值和类型。
现在让我们理解一下 ”在运行时知道变量的类型的必要“。假设我们要编写一个简单的函数,它将一个结构体作为参数,并使用这个参数创建一个SQL
插入语句。
考虑一下下面这个程序
1 | go复制代码package main |
我们需要写一个接收上面定义的结构体o
作为参数,返回类似INSERT INTO order VALUES(1234, 567)
这样的SQL
语句。这个函数定义写来很容易,比如像下面这样。
1 | go复制代码package main |
上面例子的createQuery
使用参数o
的ordId
和customerId
字段创建SQL。
现在让我们将我们的SQL
创建函数定义地更抽象些,下面还是用程序附带说明举一个案例,比如我们想泛化我们的SQL
创建函数使其适用于任何结构体。
1 | go复制代码package main |
现在我们的目标是,改造createQuery
函数,让它能接受任何结构作为参数并基于结构字段创建INSERT
语句。比如如果传给createQuery
的参数不再是order
类型的结构体,而是employee
类型的结构体时
1 | go复制代码 e := employee { |
那它应该返回的INSERT
语句应该是
1 | go复制代码INSERT INTO employee (name, id, address, salary, country) |
由于createQuery
函数要适用于任何结构体,因此它需要一个 interface{}
类型的参数。为了说明问题,简单起见,我们假定createQuery
函数只处理包含string
和 int
类型字段的结构体。
编写这个createQuery
函数的唯一方法是检查在运行时传递给它的参数的类型,找到它的字段,然后创建SQL。这里就是需要反射发挥用的地方啦。在后续步骤中,我们将学习如何使用Go
语言的反射包来实现这一点。
Go语言的反射包
Go
语言自带的reflect
包实现了在运行时进行反射的功能,这个包可以帮助识别一个interface{}
类型变量其底层的具体类型和值。我们的createQuery
函数接收到一个interface{}
类型的实参后,需要根据这个实参的底层类型和值去创建并返回INSERT
语句,这正是反射包的作用所在。
在开始编写我们的通用SQL
生成器函数之前,我们需要先了解一下reflect
包中我们会用到的几个类型和方法,接下来我们先逐个学习一下。
reflect.Type 和 reflect.Value
经过反射后interface{}
类型的变量的底层具体类型由reflect.Type
表示,底层值由reflect.Value
表示。reflect
包里有两个函数reflect.TypeOf()
和reflect.ValueOf()
分别能将interface{}
类型的变量转换为reflect.Type
和reflect.Value
。这两种类型是创建我们的SQL
生成器函数的基础。
让我们写一个简单的例子来理解这两种类型。
1 | go复制代码package main |
上面的程序会输出:
1 | go复制代码Type main.order |
上面的程序里createQuery
函数接收一个interface{}
类型的实参,然后把实参传给了reflect.Typeof
和reflect.Valueof
函数的调用。从输出,我们可以看到程序输出了interface{}
类型实参对应的底层具体类型和值。
Go语言反射的三法则
这里插播一下反射的三法则,他们是:
- 从接口值可以反射出反射对象。
- 从反射对象可反射出接口值。
- 要修改反射对象,其值必须可设置。
反射的第一条法则是,我们能够吧Go
中的接口类型变量转换成反射对象,上面提到的reflect.TypeOf
和 reflect.ValueOf
就是完成的这种转换。第二条指的是我们能把反射类型的变量再转换回到接口类型,最后一条则是与反射值是否可以被更改有关。三法则详细的说明可以去看看德莱文大神写的文章 Go反射的实现原理,文章开头就有对三法则说明的图解,再次膜拜。
下面我们接着继续了解完成我们的SQL生成器需要的反射知识。
reflect.Kind
reflect
包中还有一个非常重要的类型,reflect.Kind
。
reflect.Kind
和reflect.Type
类型可能看起来很相似,从命名上也是,Kind和Type在英文的一些Phrase是可以互转使用的,不过在反射这块它们有挺大区别,从下面的程序中可以清楚地看到。
1 | go复制代码package main |
上面的程序会输出
1 | arduino复制代码Type main.order |
通过输出让我们清楚了两者之间的区别。 reflect.Type
表示接口的实际类型,即本例中main.order
而Kind
表示类型的所属的种类,即main.order
是一个「struct」类型,类似的类型map[string]string
的Kind就该是「map」。
反射获取结构体字段的方法
我们可以通过reflect.StructField
类型的方法来获取结构体下字段的类型属性。reflect.StructField
可以通过reflect.Type
提供的下面两种方式拿到。
1 | go复制代码// 获取一个结构体内的字段数量 |
reflect.structField
是一个struct类型,通过它我们又能在反射里知道字段的基本类型、Tag、是否已导出等属性。
1 | go复制代码type StructField struct { |
与reflect.Type
提供的获取Field
信息的方法相对应,reflect.Value
也提供了获取Field
值的方法。
1 | go复制代码func (v Value) Field(i int) Value { |
这块需要注意,不然容易迷惑。下面我们尝试一下通过反射拿到order
结构体类型的字段名和值
1 | go复制代码package main |
上面的程序会输出:
1 | arduino复制代码FieldName: ordId FiledType: int FiledValue: 456 |
除了获取结构体字段名称和值之外,还能获取结构体字段的Tag,这个放在后面的文章我再总结吧,不然篇幅就太长了。
reflect.Value转换成实际值
现在离完成我们的SQL生成器还差最后一步,即还需要把reflect.Value
转换成实际类型的值,reflect.Value
实现了一系列Int()
, String()
,Float()
这样的方法来完成其到实际类型值的转换。
用反射搞一个SQL生成器
上面我们已经了解完写这个SQL生成器函数前所有的必备知识点啦,接下来就把他们串起来,加工完成createQuery
函数。
这个SQL生成器完整的实现和测试代码如下:
1 | go复制代码package main |
同学们可以把代码拿到本地运行一下,上面的例子会根据传递给函数不同的结构体实参,输出对应的标准SQL
插入语句
1 | go复制代码INSERT INTO order (ordId, customerId); VALUES (456, 56); |
总结
这篇文章通过利用反射完成一个实际应用来教会大家Go
语言反射的基本使用方法,虽然反射看起来挺强大,但使用反射编写清晰且可维护的代码非常困难,应尽可能避免,仅在绝对必要时才使用。
我的看法是如果是要写业务代码,根本不需要使用反射,如果要写类似encoding/json
,gorm
这些样的库倒是可以利用反射的强大功能简化库使用者的编码难度。
文章内容已收录到《Go开发参考书》 这个仓库里,目前已经收集了70多条开发实践。
本文转载自: 掘金