欢迎阅读Golang系列教程第34篇:反射。
反射是Go中的高级用法之一。我将尽可能用简单清晰明了的方式来讲清楚它的用法。
文章目录
什么是反射?
反射是程序在运行时检查一个变量的值和类型的这样一种功能。您可能不明白这意味着什么,但是没关系。在本教程结束时,您将对反射有了一个清晰的了解。
为什么需要检查变量并找到其类型?
任何人在学习反射时都会遇到的第一个问题就是:程序中的每个变量,在我们定义和编译时就已经知道了它的类型,为什么需要在运行时还要去检查变量并找到其类型呢?嗯,在大多数情况下都是如此,但并非总是如此。
让我解释一下我的意思。让我们编写一个简单的程序。
package main
import (
"fmt"
)
func main() {
i := 10
fmt.Printf("%d %T", i, i)
}
在上面的程序中,变量i
的类型在编译时是已知的,我们在下一行中打印它。这个很简单。
输出:
10 int
现在让我们了解下在运行时需要知道变量和类型的需求。假设我们要编写一个简单的函数,该函数将struct
作为参数并使用它创建一个SQL插入查询语句。
考虑以下程序,
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(o)
}
我们需要编写一个函数,该函数将结构体o
作为参数,并返回以下SQL插入语句:
insert into order values(1234, 567)
此函数写起来还是比较简单的,现在就开始吧。
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func createQuery(o order) string {
i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)
return i
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(createQuery(o))
}
函数createQuery
通过使用结构体o
中的ordId
和customerId
字段创建了SQL插入语句。该程序将输出:
insert into order values(1234, 567)
现在,让我们把查询生成器这个函数的难度加大。如果我们要泛化这个SQL成成函数并把它适用于任何struct
,那么该怎么办呢。让我用代码来说明一下:
package main
type order struct {
ordId int
customerId int
}
type employee struct {
name string
id int
address string
salary int
country string
}
func createQuery(q interface{}) string {
}
func main() {
}
我们的目标是完成createQuery
函数的功能,使得它就可以将任何struct
作为参数并基于这个结构体里面的字段,创建一个SQL插入语句。
比如,如果我们传递下面的结构体,
o := order {
ordId: 1234,
customerId: 567
}
我们的createQuery
函数应该返回:
insert into order values (1234, 567)
同样,如果我们这样传:
e := employee {
name: "Naveen",
id: 565,
address: "Science Park Road, Singapore",
salary: 90000,
country: "Singapore",
}
它应该返回:
insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")
由于createQuery
函数的参数可以传任何结构体,因此这个结构体参数的类型得申明为interface {}
类型。为简单起见,我们只会处理包含类型字段为string
和int
的结构体参数,当然也可以扩展到任何类型。
由于createQuery
函数的参数可以传任何结构体。编写此函数的唯一方法是在运行时检查传递给它的struct参数的类型,找到其字段和值,然后创建SQL查询语句。这是反射有用的地方。在本教程的后续步骤中,我们将学习如何使用reflect
包来实现这一目标。
reflect package
go语言中reflect
包就可以实现运行时反射。reflect
可以识别底层的具体类型和interface {}
变量的值,这正是我们所需要的。该createQuery
函数接受一个interface{}
参数,并且需要根据interface{}
参数的具体类型和值来创建查询。这正是reflect
包可以帮助实现这个功能。
在编写通用查询生成器程序之前,我们需要首先了解reflect
包中的几种类型和方法。让我们一一看下。
reflect.Type 和 reflect.Value
interface{}
的具体类型由reflect.Type
表示,值由reflect.Value
表示 。有两个函数reflect.TypeOf()
和reflect.ValueOf()
分别返回reflect.Type
和reflect.Value
。这两种类型是创建查询生成器的基础。让我们写一个简单的例子来理解这两种类型。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
t := reflect.TypeOf(q)
v := reflect.ValueOf(q)
fmt.Println("Type ", t)
fmt.Println("Value ", v)
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
在上面的程序中,第13行的createQuery
函数使用了一个interface {}
作为参数。第14行中的函数reflect.TypeOf()
使用interface {}
作为参数,并返回reflect.Type
,其中包含传递的interface {}
参数的具体类型。类似地,第15行中的reflect.ValueOf
函数使用interface {}
作为参数并返回reflect.Value
,包含所传递的interface {}
参数的值。
以上程序打印,
Type main.order
Value {456 56}
从输出中,我们可以看到程序打印了具体的类型和接口的值。
reflect.Kind
反射包中还有一种更重要的类型称为Kind。
反射包中的类型Kind和类型Type可能看起来相似,但是它们之间的区别可以从下面的程序中清楚看出。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
t := reflect.TypeOf(q)
k := t.Kind()
fmt.Println("Type ", t)
fmt.Println("Kind ", k)
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
上面的程序输出,
Type main.order
Kind struct
我想您现在会清楚两者之间的区别。Type
表示interface{}
的实际类型,在本例中为main.Order
,而Kind
表示类型的特定类型。在这种情况下,它是一个struct
。
NumField() 和 Field() 方法
NumField()
方法,返回一个结构体中字段的数量。Field(i int)
方法,返回第i
个字段的reflect.Value
。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
v := reflect.ValueOf(q)
fmt.Println("Number of fields", v.NumField())
for i := 0; i < v.NumField(); i++ {
fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
}
}
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
在上面的程序中,第14行,我们首先检查q
的Kind() 是否为
struct,因为该
NumField`方法仅适用于struct。该程序输出:
Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56
Int() 和 String()方法
Int
和String
方法分别帮助提取reflect.Valuea
为int64
和 string
package main
import (
"fmt"
"reflect"
)
func main() {
a := 56
x := reflect.ValueOf(a).Int()
fmt.Printf("type:%T value:%v\n", x, x)
b := "Naveen"
y := reflect.ValueOf(b).String()
fmt.Printf("type:%T value:%v\n", y, y)
}
在上面的程序中,在第10行和13行,我们提取
reflect.Value为int64
和string
。该程序打印:
type:int64 value:56
type:string value:Naveen
完成程序
现在我们有足够的知识来完成我们的查询生成器,让我们继续进行。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
type employee struct {
name string
id int
address string
salary int
country string
}
func createQuery(q interface{}) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
t := reflect.TypeOf(q).Name()
query := fmt.Sprintf("insert into %s values(", t)
v := reflect.ValueOf(q)
for i := 0; i < v.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
} else {
query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
} else {
query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
}
default:
fmt.Println("Unsupported type")
return
}
}
query = fmt.Sprintf("%s)", query)
fmt.Println(query)
return
}
fmt.Println("unsupported type")
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
e := employee{
name: "Naveen",
id: 565,
address: "Coimbatore",
salary: 90000,
country: "India",
}
createQuery(e)
i := 90
createQuery(i)
}
在第 22行,首先检查传递的参数是否为struct。在第 23行我们从reflect.Type
使用该Name()
方法的结构获得名称。在下一行中,我们使用t
并开始创建查询。
该案例声明符合要求。第28行检查当前字段是否为reflect.Int
,如果是这种情况,我们int64
使用Int()
方法提取该字段的值。如果其他语句用来处理边缘情形。请添加日志以了解为什么需要它。
我们还添加了检查功能,以防止将不支持的类型传递给 createQuery
函数时程序崩溃。该程序的其余部分不言自明。
该程序打印,
insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type
如何将字段名称添加到输出查询中呢?请尝试更改程序以打印格式的查询,我将其留给读者作为练习。
insert into order(ordId, customerId) values(456, 56)
答案:
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
type employee struct {
name string
id int
address string
salary int
country string
}
func createQuery(q interface{}) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
t := reflect.TypeOf(q).Name()
query := fmt.Sprintf("insert into %s(", t)
v := reflect.ValueOf(q)
//
p := reflect.TypeOf(q)
for i := 0; i < p.NumField(); i++ {
if i == 0 {
query = fmt.Sprintf("%s`%s`", query, p.Field(i).Name)
} else {
query = fmt.Sprintf("%s, `%s`", query, p.Field(i).Name)
}
}
query = fmt.Sprintf("%s) values(", query)
for i := 0; i < v.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
} else {
query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
} else {
query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
}
default:
fmt.Println("Unsupported type")
return
}
}
query = fmt.Sprintf("%s)", query)
fmt.Println(query)
return
}
fmt.Println("unsupported type")
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
e := employee{
name: "Naveen",
id: 565,
address: "Coimbatore",
salary: 90000,
country: "India",
}
createQuery(e)
i := 90
createQuery(i)
}
应该使用反射吗?
上面我们通过一些例子展示了反射的实际用途,那么问题来了?。你应该使用反射吗?我想引用罗伯·派克(Rob Pike)关于使用反射的谚语,它回答了这个问题。
Clear is better than clever. Reflection is never clear.
清晰胜于聪明。反思从未明确。
反射是Go中非常强大且先进的概念,应谨慎使用。使用反射编写清晰、可维护的代码非常困难。除非是非用不可的时候,一般情况下应尽可能避免使用它。
本次教程结尾了,希望你喜能欢。祝你有美好的一天。