Go系列教程之反射的用法
反射是Go语言的高级主题之一。我会尽可能让它变得简单易懂。
本教程分为如下小节。
- 什么是反射?
- 为何需要检查变量,确定变量的类型?
- reflect包
- reflect.Type和reflect.Value
- reflect.Kind
- NumField()和Field()方法
- Int()和String()方法
- 完整的程序
- 我们应该使用反射吗?
让我们来逐个讨论这些章节。
什么是反射?
反射就是程序能够在运行时检查变量和值,求出它们的类型。你可能还不太懂,这没关系。在本教程结束后,你就会清楚地理解反射,所以跟着我们的教程学习吧。
为何需要检查变量,确定变量的类型?
在学习反射时,所有人首先面临的疑惑就是:如果程序中每个变量都是我们自己定义的,那么在编译时就可以知道变量类型了,为什么我们还需要在运行时检查变量,求出它的类型呢?没错,在大多数时候都是这样,但并非总是如此。
我来解释一下吧。下面我们编写一个简单的程序。
packagemain import( "fmt" ) funcmain(){ i:=10 fmt.Printf("%d%T",i,i) }
在playground上运行
在上面的程序中,i的类型在编译时就知道了,然后我们在下一行打印出i。这里没什么特别之处。
现在了解一下,需要在运行时求得变量类型的情况。假如我们要编写一个简单的函数,它接收结构体作为参数,并用它来创建一个SQL插入查询。
考虑下面的程序:
packagemain import( "fmt" ) typeorderstruct{ ordIdint customerIdint } funcmain(){ o:=order{ ordId:1234, customerId:567, } fmt.Println(o) }
在playground上运行
在上面的程序中,我们需要编写一个函数,接收结构体变量o作为参数,返回下面的SQL插入查询。
insertintoordervalues(1234,567)
这个函数写起来很简单。我们现在编写这个函数。
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)) }
在playground上运行
在第12行,createQuery函数用o的两个字段(ordId和customerId),创建了插入查询。该程序会输出:
insertintoordervalues(1234,567)
现在我们来升级这个查询生成器。如果我们想让它变得通用,可以适用于任何结构体类型,该怎么办呢?我们用程序来理解一下。
packagemain typeorderstruct{ ordIdint customerIdint } typeemployeestruct{ namestring idint addressstring salaryint countrystring } funccreateQuery(qinterface{})string{ } funcmain(){ }
我们的目标就是完成createQuery函数(上述程序中的第16行),它可以接收任何结构体作为参数,根据结构体的字段创建插入查询。
例如,如果我们传入下面的结构体:
o:=order{ ordId:1234, customerId:567 }
createQuery函数应该返回:
insertintoordervalues(1234,567)
类似地,如果我们传入:
e:=employee{ name:"Naveen", id:565, address:"ScienceParkRoad,Singapore", salary:90000, country:"Singapore", }
该函数会返回:
insertintoemployeevalues("Naveen",565,"ScienceParkRoad,Singapore",90000,"Singapore")
由于createQuery函数应该适用于任何结构体,因此它接收interface{}作为参数。为了简单起见,我们只处理包含string和int类型字段的结构体,但可以扩展为包含任何类型的字段。
createQuery函数应该适用于所有的结构体。因此,要编写这个函数,就必须在运行时检查传递过来的结构体参数的类型,找到结构体字段,接着创建查询。这时就需要用到反射了。在本教程的下一步,我们将会学习如何使用reflect包来实现它。
reflect包
在Go语言中,reflect实现了运行时反射。reflect包会帮助识别interface{}变量的底层具体类型和具体值。这正是我们所需要的。createQuery函数接收interface{}参数,根据它的具体类型和具体值,创建SQL查询。这正是reflect包能够帮助我们的地方。
在编写我们通用的查询生成器之前,我们首先需要了解reflect包中的几种类型和方法。让我们来逐个了解。
reflect.Type和reflect.Value
reflect.Type表示interface{}的具体类型,而reflect.Value表示它的具体值。reflect.TypeOf()和reflect.ValueOf()两个函数可以分别返回reflect.Type和reflect.Value。这两种类型是我们创建查询生成器的基础。我们现在用一个简单的例子来理解这两种类型。
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) }
在playground上运行
在上面的程序中,第13行的createQuery函数接收interface{}作为参数。在第14行,reflect.TypeOf接收了参数interface{},返回了reflect.Type,它包含了传入的interface{}参数的具体类型。同样地,在第15行,reflect.ValueOf函数接收参数interface{},并返回了reflect.Value,它包含了传来的interface{}的具体值。
上述程序会打印:
Type main.order
Value {45656}
从输出我们可以看到,程序打印了接口的具体类型和具体值。
relfect.Kind
reflect包中还有一个重要的类型:Kind。
在反射包中,Kind和Type的类型可能看起来很相似,但在下面程序中,可以很清楚地看出它们的不同之处。
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) }
在playground上运行
上述程序会输出:
Type main.order
Kind struct
我想你应该很清楚两者的区别了。Type表示interface{}的实际类型(在这里是main.Order),而Kind表示该类型的特定类别(在这里是struct)。
NumField()和Field()方法
NumField()方法返回结构体中字段的数量,而Field(iint)方法返回字段i的reflect.Value。
packagemain import( "fmt" "reflect" ) typeorderstruct{ ordIdint customerIdint } funccreateQuery(qinterface{}){ ifreflect.ValueOf(q).Kind()==reflect.Struct{ v:=reflect.ValueOf(q) fmt.Println("Numberoffields",v.NumField()) fori:=0;i在playground上运行
在上面的程序中,因为NumField方法只能在结构体上使用,我们在第14行首先检查了q的类别是struct。程序的其他代码很容易看懂,不作解释。该程序会输出:
Numberoffields2
Field:0type:reflect.Valuevalue:456
Field:1type:reflect.Valuevalue:56
Int()和String()方法
Int和String可以帮助我们分别取出reflect.Value作为int64和string。
packagemain import( "fmt" "reflect" ) funcmain(){ a:=56 x:=reflect.ValueOf(a).Int() fmt.Printf("type:%Tvalue:%v\n",x,x) b:="Naveen" y:=reflect.ValueOf(b).String() fmt.Printf("type:%Tvalue:%v\n",y,y) }在playground上运行
在上面程序中的第10行,我们取出reflect.Value,并转换为int64,而在第13行,我们取出reflect.Value并将其转换为string。该程序会输出:
type:int64value:56
type:stringvalue:Naveen
完整的程序
现在我们已经具备足够多的知识,来完成我们的查询生成器了,我们来实现它把。
packagemain import( "fmt" "reflect" ) typeorderstruct{ ordIdint customerIdint } typeemployeestruct{ namestring idint addressstring salaryint countrystring } funccreateQuery(qinterface{}){ ifreflect.ValueOf(q).Kind()==reflect.Struct{ t:=reflect.TypeOf(q).Name() query:=fmt.Sprintf("insertinto%svalues(",t) v:=reflect.ValueOf(q) fori:=0;i在playground上运行
在第22行,我们首先检查了传来的参数是否是一个结构体。在第23行,我们使用了Name()方法,从该结构体的reflect.Type获取了结构体的名字。接下来一行,我们用t来创建查询。
在第28行,case语句检查了当前字段是否为reflect.Int,如果是的话,我们会取到该字段的值,并使用Int()方法转换为int64。ifelse语句用于处理边界情况。请添加日志来理解为什么需要它。在第34行,我们用来相同的逻辑来取到string。
我们还作了额外的检查,以防止createQuery函数传入不支持的类型时,程序发生崩溃。程序的其他代码是自解释性的。我建议你在合适的地方添加日志,检查输出,来更好地理解这个程序。
该程序会输出:
insertintoordervalues(456,56) insertintoemployeevalues("Naveen",565,"Coimbatore",90000,"India") unsupportedtype至于向输出的查询中添加字段名,我们把它留给读者作为练习。请尝试着修改程序,打印出以下格式的查询。
insertintoorder(ordId,customerId)values(456,56)我们应该使用反射吗?
我们已经展示了反射的实际应用,现在考虑一个很现实的问题。我们应该使用反射吗?我想引用RobPike关于使用反射的格言,来回答这个问题。
清晰优于聪明。而反射并不是一目了然的。
反射是Go语言中非常强大和高级的概念,我们应该小心谨慎地使用它。使用反射编写清晰和可维护的代码是十分困难的。你应该尽可能避免使用它,只在必须用到它时,才使用反射。
本教程到此结束。希望你们喜欢。祝你愉快。希望对大家的学习有所帮助,也希望大家多多支持毛票票。