Go中的反射
本文是《Go系列教程》的第二十六篇文章。
反射是Go的高级特性之一。我将尽力讲解地简单些。
本教程具有下面几个章节:
什么是反射?
考察一个变量,并且寻找它的类型的需要是什么?
-
反射包
. reflect.Type 和 reflect.Value
. reflect.Kind
. NumField() 和 Field() methods
. Int() 和 String() methods
完整的程序
应该使用反射吗?
我们将对这些内容一个个地加以讨论。
什么是反射?
反射指的是一个程序可以在运行时检查变量以及它的值并查找他们的类型。你可能不太理解这是什么意思,不过,没事。在读完本文章之后,你会有一个清晰的理解。来跟着我一探究竟。
检查变量并查找其类型的需要是什么?
许多人在学习反射的时候,都有一个问题,那就是为什么我们需要在运行时期检查变量,查找它的类型呢。我们程序中的每一个变量都是由我们自己定义的。我们在编译期就知道它的类型。确实,在大多数情况下是这样,但并不是所有时候都是如此。
我们写个简单的程序,来解释一下。
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函数使用o的ordId字段和customerId字段创建了一个插入语句。程序的输出如下:
insert into order values(1234, 567)
此时,我们让这个函数更高级一点。如果我们想让这个函数可以接收任何的结构体,并根据结构体生成SQL语句该怎么办。我用程序来解释一下。
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函数,让它可以接收任何的结构体作为参数,并基于结构体的字段生成相应的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函数应该接受任意的结构体参数的话,那也就意味着,它接收一个接口作为参数。简单起见,我们只处理含有string和int类型的字段。
createQuery可以处理所有的结构体,那么编写此函数的唯一的方式就是在运行时检查此结构体的类型,并找到它都包含哪些字段,据此创建SQL语句。这时候,反射就十分有用了。
下一步,我们就要学习下如何使用反射包来实现这个需求。
反射包
Go的反射包实现了运行时反射。反射包可用于识别底层的具体数据类型,以及一个接口变量的值。而这正是我们所需要的。createQuery函数会接收一个接口参数,基于接口的值以及它的具体类型,我们会创建对应的SQL语句。反射包正好能帮助我们做到这一点。
在写我们的程序之前,我们要先了解一下反射包中的几个类以及相关的方法。我们先一个个看下。
reflect.Type 和reflect.Value
reflect.Type可以代表interface{}的具体类型,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)
}
上面的这个程序中,createQuery函数接收了一个interface{}作为参数。reflect.TypeOf(q)接收一个interface{}参数,并返回它的此接口的具体数据类型。同样, reflect.ValueOf 接收了一个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代表的是接口的实际类型,这里即:main.Order,而Kind代表的是类型的种类,此处即为:struct。
NumField()和Field()方法
NumFiled()方法会返回结构体中的字段的个数,而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,因为NumFiled方法只对strcut有用。程序的其余部分都比较好理解。程序的输出如下:
Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56
Int()和String()方法
Int方法和String()方法可用以提取int64和string的reflect.Value的值。
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)
}
在上面的程序中,我们提取了int64的reflect.Value的值,以及String的reflect.Value的值。程序输出如下:
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)
}
程序的输出如下:
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的一个高级的特性。用它的时候需要特别小心。如果使用反射的话,我们写的代码就不是那么好理解,好维护。我们应尽力避免使用反射,除非必要时。
感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!
备注
本文系翻译之作原文博客地址