反射是 Go 的难度比较大的知识点之一。我会尽量介绍的时候简单一点。
本教程有以下几节。
什么是反射?
检查变量并找到其类型需要的是什么?
反射的包
reflect.Type 和 reflect.Value
reflect.Kind
NumField() 和 Field() 方法
Int() 和 String() 方法
完整的程序
是否应该使用反射?
现在让我们一一讨论这些部分。
什么是反射?
反射是程序在运行时检查变量和值并获取到它们的类型的能力。你可能不明白这是什么意思,但没关系。在本教程的最后,你将对反射会有一个清晰的了解,所以请继续往下面看。
检查变量并找到其类型的条件是什么?
在学习反射时,任何人的第一个问题是为什么我们需要检查变量并在运行时获取它的类型?当我们的程序中的每个变量都由我们定义时,我们在编译时就知道它的类型。嗯,大部分时候都是这样,但并非总是如此。
让我解释一下我的意思。我们来写一个简单的程序。
package main
import (
"fmt"
)
func main() {
i := 10
fmt.Printf("%d %T", i, i)
}
在上面的程序中,i
的类型在编译时是已知的,我们在下一行打印它。这里没什么好奇怪的。
现在让我们了解在运行时知道变量类型的必要性。假设我们想编写一个简单的函数,它将 struct 作为参数,并使用它创建一个 SQL 插入查询。
思考下面的程序,
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(o)
}
我们需要编写一个函数,它将上面程序中的 struct 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
函数。 第12行中使用 o
的 ordId
和 customerId
字段创建插入查询。该程序将输出,
insert into order values(1234, 567)
现在让我们的查询创建进入下一个阶段。如果我们想要使我们的查询创建程序适用于任何结构,该怎么办?让我解释一下这是什么意思。
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() {
}
上面程序第 16 行中,我们的目标是在第一行完成 createQuery
函数。以便它将任何 struct
作为参数,并基于 struct
字段创建插入查询。
例如,如果我们传递下面的结构,
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 参数的类型,找到它的字段然后创建查询。这是反射有用的地方。在本教程的后续步骤中,我们将学习如何使用 reflect
包实现此目的。
reflect package
反射包在 Go 中实现运行时反射。反射包有助于识别底层具体类型和 interface{} 变量的值。这正是我们所需要的。 createQuery
函数采用 interface{}
参数,需要根据 interface{}
参数的具体类型和值创建查询。这正是反射包所帮助做的。
在编写通用查询生成器程序之前,我们需要首先了解反射包中的一些类型和方法。让我们一个一个介绍。
reflect.Type and 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{} 作为参数,并返回包含传递的 interface{} 参数的具体类型的 reflect.Type。类似地,第 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.Value
分别提取为 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)
}
在上面的程序中,第 11 行我们将 reflect.Value
提取为 int64
,然后第 13 行将其提取为 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 行我们使用 Name()
方法从 reflect.Type
获取结构的名称。 在下一行中,我们使用 t
并开始创建查询。
case 语句在第28行检查当前字段是否为 reflect.Int
,如果是这种情况,我们使用Int()
方法将该字段的值提取为 int64
。 if else 语句用于处理边界情况。 请添加日志来了解为何需要它。 34 行中类似的逻辑用于提取 string
。
我们还添加了一些检查,以防止在将不支持的类型传递给 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)
是否应该使用反射?
展示了反射的实际用途,现在才是真正的问题。 你应该使用反射吗? 我想引用 Rob Pike 关于使用反射的格言来回答这个问题。
Clear is better than clever. Reflection is never clear.
在Go中,反射是一个非常强大和先进的概念,应该谨慎使用。 使用反射编写清晰且可维护的代码非常困难。 应尽可能避免使用,并且只有在绝对必要时才应使用反射。