反射是Go中的一个高级话题。我将尽可能地使它简单化。
本教程有以下几个部分。
- 什么是反射?
- 有什么必要检查一个变量并找到它的类型?
- 反射包
- reflect.Type和reflect.Value
- reflect.Kind
- NumField()和Field()方法
- Int()和String()方法
- 完整的程序
- 应该使用反射吗?
现在让我们逐一讨论这些部分。
什么是反射?
反射是指程序在运行时检查其变量和数值并找到其类型的能力。你可能不明白这意味着什么,但这没有关系。在本教程结束时,你会对反射有一个清晰的认识,所以请跟着我。
为什么要检查一个变量并找到其类型呢?
任何人在学习反射时都会遇到的第一个问题是,既然我们程序中的每一个变量都是由我们自己定义的,而且我们在编译时就知道它的类型,为什么还要在运行时检查变量并找到它的类型?嗯,大多数时候是这样的,但并不总是这样。
让我解释一下我的意思。让我们写一个简单的程序。
1 | package main |
在上面的程序中,i
的类型在编译时就已经知道了,我们在下一行打印它。这里没有什么神奇之处。
现在我们来了解一下在运行时知道变量类型的必要性。假设我们想写一个简单的函数,该函数将接受一个结构体作为参数,并使用它创建一个SQL插入查询。
请考虑以下程序。
1 | package main |
我们需要写一个函数,将上述程序中的结构o
作为参数,并返回以下SQL插入查询。
1 | insert into order values(1234, 567) |
这个函数的编写很简单。我们现在就来做这件事。
1 | package main |
第12行的createQuery
函数通过使用o
的ordId
和customerId
字段创建插入查询,这个程序将输出:
1 | insert into order values(1234, 567) |
现在让我们把我们的查询创建器提高到一个新的水平。如果我们想使我们的查询创建器通用化,并使其适用于任何结构,该怎么办。让我解释一下我用程序的意思。
1 | package main |
我们的目标是完成上述程序第16行的createQuery
函数,使其接受任何结构体作为参数,并根据结构体字段创建一个插入查询。
例如,如果我们传递下面的结构。
1 | o := order { |
我们的createQuery
函数应该返回。
1 | insert into order values (1234, 567) |
同样地,如果我们通过
1 | e := employee { |
它应该返回。
1 | insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore") |
由于createQuery
函数应适用于任何结构,它需要一个interface{}
作为参数。为了简单起见,我们将只处理包含string
和int
类型字段的结构,但这可以扩展到任何类型。
createQuery
函数应适用于任何结构。编写这个函数的唯一方法是在运行时检查传递给它的结构参数的类型,找到其字段,然后创建查询。这就是反射的用处。在本教程的下一步,我们将学习如何使用反射包实现这一目标。
反射包
反射包实现了Go中的运行时反射。反射包有助于识别底层的具体类型和interface{}
变量的值。这正是我们所需要的。createQuery
函数需要一个interface{}
参数,查询需要根据interface{}
参数的具体类型和值来创建。这正是反射包所帮助完成的。
在编写我们的通用查询生成器程序之前,我们需要首先了解反射包中的一些类型和方法。让我们一个一个地看一下它们。
reflect.Type和reflect.Value
interface{}
的具体类型由 reflect.Type
表示,底层值由 reflect.Value
表示。有两个函数 reflect.TypeOf()
和 reflect.ValueOf()
分别返回 reflect.Type
和 reflect.Value。
这两个类型是创建我们的查询生成器的基础。让我们写一个简单的例子来理解这两种类型。
1 | package main |
在上面的程序中,第13行的createQuery
函数以interface{}
为参数。13行的createQuery
函数需要一个interface{}
作为参数。第14行的函数reflect.TypeOf
以interface{}
为参数,并返回包含所传递的interface{}
参数的具体类型的reflect.Type
。同样地,第15行的reflect.ValueOf
函数以interface{}
为参数,并返回reflect.Value
,其中包含所传递的interface{}
参数的基本值。
上述程序打印出。
1 | Type main.order |
从输出中,我们可以看到,程序打印了接口的具体类型和值。
reflect.Kind
在反射包中还有一个重要的类型叫Kind。
反射包中的类型Kind
和Type
看起来很相似,但它们有一个区别,从下面的程序中就可以看出。
1 | package main |
上面的程序输出。
1 | Type main.order |
我想你现在会清楚这两者之间的区别了。Type
表示interface{}
的实际类型,在本例中是main.Order
,Kind
表示该类型的具体种类。在本例中,它是一个struct
。
Kind既可以为
reflect.TypeOfKind
也可以为reflect.ValueOf.Kind
NumField()和Field()方法
NumField()
方法返回结构体中字段的数量,Field(i int)
方法返回第i
个字段的reflect.Value
。
1 | package main |
在上面的程序中,在第14行,我们首先检查q
的类型是否为struct
,因为NumField
方法只对结构体起作用。程序的其余部分是不言自明的。这个程序的输出。
1 | Number of fields 2 |
Int()和String()方法
Int
和String
方法分别帮助将reflect.Value
提取为int64
和string
。
1 | package main |
在上面的程序中,在第10行,我们将reflect.Value
提取为int64
,在第13行,我们将其提取为string
,这个程序打印:
1 | type:int64 value:56 |
完成程序
现在我们有足够的知识来完成我们的查询生成器,让我们继续做吧。
1 | package main |
在第22行,我们首先检查传递的参数是否是一个struct
,第28行的case语句检查当前字段是否为reflect.Int
,如果是,我们使用Int()
方法提取该字段的值为int64
。if else语句是用来处理边缘情况的。请添加日志(logs)以了解为什么需要它。类似的逻辑被用于提取第34行的string
。
我们还添加了检查,以防止程序在不支持的类型被传递给createQuery
函数时崩溃。该程序的其余部分是不言自明的。我建议在适当的地方添加日志,并检查其输出,以更好地理解这个程序。
这个程序可以打印出来。
1 | insert into order values(456, 56) |
我想让你们把字段名添加到输出的查询中,作为一个练习。请尝试改变程序以打印格式的查询。
1 | insert into order(ordId, customerId) values(456, 56) |
应该使用反射吗?
在展示了反射的实际用途之后,现在是真正的问题。你应该使用反思吗?我想引用Rob Pike关于使用反射的谚语来回答这个问题。
lear is better than clever. Reflection is never clear.
反射在Go中是一个非常强大和高级的概念,应该谨慎使用。使用反射编写清晰和可维护的代码是非常困难的。应该尽可能避免使用它,只有在绝对必要时才使用。
至此,本教程结束。希望你喜欢它。祝你有一个愉快的一天。