牛x!搞明白 Go 反射的使用方法和应用场景

发布于 2021-11-04 13:20 ,所属分类:软件编程学习资料

今天来聊一个平时用的不多,但是很多框架或者基础库会用到的语言特性--反射,反射并不是Go语言独有的能力,其他编程语言都有。这篇文章的目标是简单地给大家梳理一下反射的应用场景和使用方法。

我们平时写代码能接触到与反射联系比较紧密的一个东西是结构体字段的标签,这个我准备放在后面的文章再梳理。

我准备通过用反射搞一个通用的SQL构造器的例子,带大家掌握反射这个知识点。这个是看了国外一个博主写的例子,觉得思路很好,我又对其进行了改进,让构造器的实现更丰富了些。

本文的思路参考自:https://golangbot.com/reflection/ ,本文内容并非只是对原文的简单翻译,具体看下面的内容吧~!

什么是反射

反射是程序在运行时检查其变量和值并找到它们类型的能力。听起来比较笼统,接下来我通过文章的例子一步步带你认识反射。

为什么需要反射

当学习反射的时候,每个人首先会想到的问题都是 “为什么我们要在运行时检查变量的类型呢,程序里的变量在定义的时候我们不都已经给他们指定好类型了吗?” 确实是这样的,但也并非总是如此,看到这你可能心里会想,大哥,你在说什么呢,em... 还是先写一个简单的程序,解释一下。

packagemain

import(
"fmt"
)

funcmain(){
i:=10
fmt.Printf("%d%T",i,i)
}

在上面的程序里, 变量i的类型在编译时是已知的,我们在下一行打印了它的值和类型。

现在让我们理解一下 ”在运行时知道变量的类型的必要“。假设我们要编写一个简单的函数,它将一个结构体作为参数,并使用这个参数创建一个SQL插入语句。

考虑一下下面这个程序

packagemain

import(
"fmt"
)

typeorderstruct{
ordIdint
customerIdint
}

funcmain(){
o:=order{
ordId:1234,
customerId:567,
}
fmt.Println(o)
}

我们需要写一个接收上面定义的结构体o作为参数,返回类似INSERT INTO order VALUES(1234, 567)这样的SQL语句。这个函数定义写来很容易,比如像下面这样。

packagemain

import(
"fmt"
)

typeorderstruct{
ordIdint
customerIdint
}

funccreateQuery(oorder)string{
i:=fmt.Sprintf("INSERTINTOorderVALUES(%d,%d)",o.ordId,o.customerId)
returni
}

funcmain(){
o:=order{
ordId:1234,
customerId:567,
}
fmt.Println(createQuery(o))
}

上面例子的createQuery使用参数oordIdcustomerId字段创建SQL。

现在让我们将我们的SQL创建函数定义地更抽象些,下面还是用程序附带说明举一个案例,比如我们想泛化我们的SQL创建函数使其适用于任何结构体。

packagemain

typeorderstruct{
ordIdint
customerIdint
}

typeemployeestruct{
namestring
idint
addressstring
salaryint
countrystring
}

funccreateQuery(qinterface{})string{
}

现在我们的目标是,改造createQuery函数,让它能接受任何结构作为参数并基于结构字段创建INSERT 语句。比如如果传给createQuery的参数不再是order类型的结构体,而是employee类型的结构体时

e:=employee{
name:"Naveen",
id:565,
address:"ScienceParkRoad,Singapore",
salary:90000,
country:"Singapore",
}

那它应该返回的INSERT语句应该是

INSERTINTOemployee(name,id,address,salary,country)
VALUES("Naveen",565,"ScienceParkRoad,Singapore",90000,"Singapore")

由于createQuery 函数要适用于任何结构体,因此它需要一个 interface{}类型的参数。为了说明问题,简单起见,我们假定createQuery函数只处理包含stringint 类型字段的结构体。

编写这个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.Typereflect.Value。这两种类型是创建我们的SQL生成器函数的基础。

让我们写一个简单的例子来理解这两种类型。

packagemain

import(
"fmt"
"reflect"
)

typeorderstruct{
ordIdint
customerIdint
}

funccreateQuery(qinterface{}){
t:=reflect.TypeOf(q)
v:=reflect.ValueOf(q)
fmt.Println("Type",t)
fmt.Println("Value",v)


}
funcmain(){
o:=order{
ordId:456,
customerId:56,
}
createQuery(o)

}

上面的程序会输出:

Typemain.order
Value{45656}

上面的程序里createQuery函数接收一个interface{}类型的实参,然后把实参传给了reflect.Typeofreflect.Valueof 函数的调用。从输出,我们可以看到程序输出了interface{}类型实参对应的底层具体类型和值。

Go语言反射的三法则

这里插播一下反射的三法则,他们是:

  1. 从接口值可以反射出反射对象。
  2. 从反射对象可反射出接口值。
  3. 要修改反射对象,其值必须可设置。

反射的第一条法则是,我们能够吧Go中的接口类型变量转换成反射对象,上面提到的reflect.TypeOfreflect.ValueOf 就是完成的这种转换。第二条指的是我们能把反射类型的变量再转换回到接口类型,最后一条则是与反射值是否可以被更改有关。三法则详细的说明可以去看看德莱文大神写的文章 Go反射的实现原理,文章开头就有对三法则说明的图解,再次膜拜。

下面我们接着继续了解完成我们的SQL生成器需要的反射知识。

reflect.Kind

reflect包中还有一个非常重要的类型,reflect.Kind

reflect.Kindreflect.Type类型可能看起来很相似,从命名上也是,Kind和Type在英文的一些Phrase是可以互转使用的,不过在反射这块它们有挺大区别,从下面的程序中可以清楚地看到。

packagemain
import(
"fmt"
"reflect"
)

typeorderstruct{
ordIdint
customerIdint
}

funccreateQuery(qinterface{}){
t:=reflect.TypeOf(q)
k:=t.Kind()
fmt.Println("Type",t)
fmt.Println("Kind",k)


}
funcmain(){
o:=order{
ordId:456,
customerId:56,
}
createQuery(o)

}

上面的程序会输出

Typemain.order
Kindstruct

通过输出让我们清楚了两者之间的区别。reflect.Type 表示接口的实际类型,即本例中main.orderKind表示类型的所属的种类,即main.order是一个「struct」类型,类似的类型map[string]string的Kind就该是「map」。

反射获取结构体字段的方法

我们可以通过reflect.StructField类型的方法来获取结构体下字段的类型属性。reflect.StructField可以通过reflect.Type提供的下面两种方式拿到。

//获取一个结构体内的字段数量
NumField()int
//根据index获取结构体内字段的类型对象
Field(iint)StructField
//根据字段名获取结构体内字段的类型对象
FieldByName(namestring)(StructField,bool)

reflect.structField是一个struct类型,通过它我们又能在反射里知道字段的基本类型、Tag、是否已导出等属性。

typeStructFieldstruct{
Namestring
TypeType//fieldtype
TagStructTag//fieldtagstring
......
}

reflect.Type提供的获取Field信息的方法相对应,reflect.Value也提供了获取Field值的方法。

func(vValue)Field(iint)Value{
...
}

func(vValue)FieldByName(namestring)Value{
...
}

这块需要注意,不然容易迷惑。下面我们尝试一下通过反射拿到order结构体类型的字段名和值

packagemain

import(
"fmt"
"reflect"
)

typeorderstruct{
ordIdint
customerIdint
}

funccreateQuery(qinterface{}){
t:=reflect.TypeOf(q)
ift.Kind()!=reflect.Struct{
panic("unsupportedargumenttype!")
}
v:=reflect.ValueOf(q)
fori:=0;i<t.NumField();i++{
fmt.Println("FieldName:",t.Field(i).Name,"FiledType:",t.Field(i).Type,
"FiledValue:",v.Field(i))
}

}
funcmain(){
o:=order{
ordId:456,
customerId:56,
}
createQuery(o)

}

上面的程序会输出:

FieldName:ordIdFiledType:intFiledValue:456
FieldName:customerIdFiledType:intFiledValue:56

除了获取结构体字段名称和值之外,还能获取结构体字段的Tag,这个放在后面的文章我再总结吧,不然篇幅就太长了。

reflect.Value转换成实际值

现在离完成我们的SQL生成器还差最后一步,即还需要把reflect.Value转换成实际类型的值,reflect.Value实现了一系列Int(), String()Float()这样的方法来完成其到实际类型值的转换。

用反射搞一个SQL生成器

上面我们已经了解完写这个SQL生成器函数前所有的必备知识点啦,接下来就把他们串起来,加工完成createQuery函数。

这个SQL生成器完整的实现和测试代码如下:

packagemain

import(
"fmt"
"reflect"
)

typeorderstruct{
ordIdint
customerIdint
}

typeemployeestruct{
namestring
idint
addressstring
salaryint
countrystring
}

funccreateQuery(qinterface{})string{
t:=reflect.TypeOf(q)
v:=reflect.ValueOf(q)
ifv.Kind()!=reflect.Struct{
panic("unsupportedargumenttype!")
}
tableName:=t.Name()//通过结构体类型提取出SQL的表名
sql:=fmt.Sprintf("INSERTINTO%s",tableName)
columns:="("
values:="VALUES("
fori:=0;i<v.NumField();i++{
//注意reflect.Value也实现了NumField,Kind这些方法
//这里的v.Field(i).Kind()等价于t.Field(i).Type.Kind()
switchv.Field(i).Kind(){
casereflect.Int:
ifi==0{
columns+=fmt.Sprintf("%s",t.Field(i).Name)
values+=fmt.Sprintf("%d",v.Field(i).Int())
}else{
columns+=fmt.Sprintf(",%s",t.Field(i).Name)
values+=fmt.Sprintf(",%d",v.Field(i).Int())
}
casereflect.String:
ifi==0{
columns+=fmt.Sprintf("%s",t.Field(i).Name)
values+=fmt.Sprintf("'%s'",v.Field(i).String())
}else{
columns+=fmt.Sprintf(",%s",t.Field(i).Name)
values+=fmt.Sprintf(",'%s'",v.Field(i).String())
}
}
}
columns+=");"
values+=");"
sql+=columns+values
fmt.Println(sql)
returnsql
}

funcmain(){
o:=order{
ordId:456,
customerId:56,
}
createQuery(o)

e:=employee{
name:"Naveen",
id:565,
address:"Coimbatore",
salary:90000,
country:"India",
}
createQuery(e)
}

同学们可以把代码拿到本地运行一下,上面的例子会根据传递给函数不同的结构体实参,输出对应的标准SQL插入语句

INSERTINTOorder(ordId,customerId);VALUES(456,56);
INSERTINTOemployee(name,id,address,salary,country);VALUES('Naveen',565,'Coimbatore',90000,'India');

总结

这篇文章通过利用反射完成一个实际应用来教会大家Go语言反射的基本使用方法,虽然反射看起来挺强大,但使用反射编写清晰且可维护的代码非常困难,应尽可能避免,仅在绝对必要时才使用。

我的看法是如果是要写业务代码,根本不需要使用反射,如果要写类似encoding/jsongorm这些样的库倒是可以利用反射的强大功能简化库使用者的编码难度。


最后,再打个广告吧。

双十一快到了,阿里云也开始搞活动了,刚好我这边可以带大家白Piao阿里云的服务器。

说白了就是大家可以一分钱不花,就可以领到服务器,规格是 2c2g(2vcpu 2G memory) 的机器。

有需要的可以加我下,备注『服务器,我统一拉群,带大家一起薅羊毛。


相关资源