反射是Go中的高级语法,本文将以尽可能用简单的方式来介绍它。
本文包括以下部分:
- 什么是反射?
- 如何检查一个变量及其类型?
- 反射包
- 完整代码
- 应该用反射吗?
现在我们一个一个地讨论这些部分。
什么是反射?
反射是程序在运行时能够检查变量、变量值和变量类型的能力。现在你可能不理解什么意思,但是不要紧。在读完本文,您将对反射有一个清晰的理解,所以请继续跟随我。
检查一个变量及其类型需要什么?
在学习反射时每个人碰到第一个问题就是程序运行时为何需要检查变量类型,程序中的每一个变量都是由我们定义的,并且我们在编译时就知道它的类型。这在大多数情况下是正确的,但并不总是如此。
让我们写一个简单的程序,来解释下。
package main
import (
"fmt"
)
func main() {
i := 10
fmt.Printf("%d %T", i, i)
}
在上面的代码中,变量i的类型在编译的时候就知道了,并且在代码中打印。没什么特别的。
现在我们来看为何在运行时需要知道变量的类型。假设我们想要编写一个简单的函数,它接受一个结构体作为参数,并使用它创建一个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函数使用结构体的ordId和customerId字段生成sql查询语句。程序输出为:
insert into order values(1234, 567)
现在将createQuery函数再升级一下。如果我们想要泛化createQuery函数,并让它在任何结构体上工作,该怎么办?我解释一下我所说的是什么意思。
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函数能够接收任何结构体为参数,并根据结构体字段来创建查询语句。
假设,传入以下结构体:
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函数对任意结构体类型都生效。唯一的办法就是在运行时检查结构体参数的类型,并找到各字段后,创建查询语句。这就是反射的作用。下一步,我们将学习如何使用reflect包来实现。
reflect包
reflect包实现了Go运行时反射。反射包能够识别interface{}变量的底层类型和值。这也是我们需要的。createQuery函数接收interface{}类型参数,需要根据其具体类型和值来创建查询语句。也是reflect包实现的功能。
在编写通用createQuery函数之前,我们需要了解反射包中的一些类型和方法。让我们一个一个来看看。
reflect.Type和reflect.Value
interface{}变量具体类型由reflect.Type表示,而reflect.Value表示其具体值。reflect.TypeOf()函数和reflect.ValueOf()函数分别返回reflect.Type和reflect.Value。这两种类型是实现createQuery函数的基础。让我们编写一个简单的示例来理解这两种类型。
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)
}
上面的代码,createQuery函数接收interface{}类型参数。reflect.TypeOf接收interface{}类型并返回reflect.Type,包含interface{}参数的具体类型。类似地,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)
}
以上代码,我们先检查参数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)
}
代码输出:
type:int64 value:56
type:string value:Naveen
完整代码
现在我们已经有了足够的知识来完成createQuery函数,让我们继续完成它。
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)
}
以上代码,我们先检查传入的参数是否为struct类型。然后使用reflect.Type的Name()方法获取结构体名称。并使用switch—case来判断各字段的具体类型,分别处理。
我们还添加了检查,防止将不支持的类型传递给createQuery函数时程序崩溃。程序的其余部分很好理解。我建议在适当的位置添加日志,并检查它们的输出,以便更好地理解这个程序。
程序输出为:
insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type
将字段名添加到输出查询中,留给读者作为练习。请尝试更改程序打印查询的格式,
应该使用反射吗?
在展示了反射的实际应用之后,现在真正的问题来了。应该使用反射吗?我想引用Rob Pike的话来回答这个问题。
Clear is better than clever. Reflection is never clear.
反射在Go中是一个非常强大和先进的概念,应该谨慎使用。使用反射编写清晰和可维护的代码是非常困难的。在任何可能的情况下都应该避免使用,只有在绝对必要的时候才应该使用。
网友评论