本文的思路参考自:https://golangbot.com/reflection/ ,本文内容并非只是对原文的简单翻译,具体看下面的内容吧~!
准备通过用反射搞一个通用的SQL
构造器的例子,带大家理解掌握反射知识点。看了国外一个博主写的例子,觉得思路很好,我又对其进行了改进,让构造器的实现更丰富了些。
什么是反射?
答:反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能
为什么需要反射?
golang反射理解 - Go语言中文网 - Golang中文社区
当学习反射的时候,每个人首先会想到的问题都是 “为什么我们要在运行时检查变量的类型呢,程序里的变量在定义的时候我们不都已经给他们指定好类型了吗?” 确实是这样的,但也并非总是如此,看到这你可能心里会想,大哥,你在说什么呢,em... 还是先写一个简单的程序,解释一下。
package main
import (
"fmt"
)
func main() {
a := 100
fmt.Printf("%d %T", a, a)
}
在上面的程序里, 变量a的类型在编译时是已知的,我们在下一行打印了它的值和类型。
让我们理解一下 ”在运行时知道变量的类型的必要“。假设我们要编写一个简单的函数,它将一个结构体作为参数,并使用这个参数创建一个SQL
插入语句。
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func main() {
o := order{
ordId: 20211103000001,
customerId: 567,
}
fmt.Println(o)
}
我们需要写一个接收上面定义的结构体o
作为参数,返回类似INSERT INTO order VALUES(20211103000001, 567)
这样的SQL
语句。这个函数定义写来很容易,比如像下面这样。
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: 20211103000001,
customerId: 567,
}
fmt.Println(createQuery(o))
}
上面例子的createQuery
使用参数o
的ordId
和customerId
字段创建SQL。
现在让我们将我们的SQL
创建函数定义地更抽象些,下面还是用程序附带说明举一个案例,比如我们想泛化我们的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 {
}
现在我们的目标是,改造createQuery
函数,让它能接受任何结构作为参数并基于结构字段创建INSERT
语句。比如如果传给createQuery
的参数不再是order
类型的结构体,而是employee
类型的结构体时
e := employee {
name: "Naveen",
id: 565,
address: "Science Park Road, Singapore",
salary: 90000,
country: "Singapore",
}
### 那它应该返回的INSERT语句应该是
INSERT INTO employee (name, id, address, salary, country)
VALUES("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")
由于createQuery
函数要适用于任何结构体,因此它需要一个 interface{}
类型的参数。为了说明问题,简单起见,我们假定createQuery
函数只处理包含string
和 int
类型字段的结构体。
编写这个createQuery
函数的唯一方法是检查在运行时传递给它的参数的类型,找到它的字段,然后创建SQL。这里就是需要反射发挥用的地方啦。在后续步骤中,我们将学习如何使用Go
语言的反射包来实现这一点。
Go语言的反射包
Go
语言自带的reflect
包实现了在运行时进行反射的功能,这个包可以帮助识别一个interface{}
类型变量其底层的具体类型和值。我们的createQuery
函数接收到一个interface{}
类型的实参后,需要根据这个实参的底层类型和值去创建并返回INSERT
语句,这正是反射包的作用所在。
在开始编写我们的通用SQL
生成器函数之前,我们需要先了解一下reflect
包中我们会用到的几个类型和方法,接下来我们先逐个学习一下。
reflect.Type 和 reflect.Value
Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国
经过反射后interface{}
类型的变量的底层具体类型由reflect.Type
表示,底层值由reflect.Value
表示。reflect
包里有两个函数reflect.TypeOf()
和reflect.ValueOf()
分别能将interface{}
类型的变量转换为reflect.Type
和reflect.Value
。这两种类型是创建我们的SQL
生成器函数的基础。
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)
}
运行输出:
Type main.order
Value {456 56}
上面的程序里createQuery
函数接收一个interface{}
类型的实参,然后把实参传给了reflect.Typeof
和reflect.Valueof
函数的调用。从输出,我们可以看到程序输出了interface{}
类型实参对应的底层具体类型和值。
Go语言反射的三法则
反射的三法则,他们是:
-
从接口值可以反射出反射对象。
-
从反射对象可反射出接口值。
-
要修改反射对象,其值必须可设置。
反射的第一条法则是,我们能够吧Go
中的接口类型变量转换成反射对象,上面提到的reflect.TypeOf
和 reflect.ValueOf
就是完成的这种转换。第二条指的是我们能把反射类型的变量再转换回到接口类型,最后一条则是与反射值是否可以被更改有关。
下面我们接着继续了解完成我们的SQL生成器需要的反射知识。
reflect.Kind
reflect
包中还有一个非常重要的类型,reflect.Kind
。
reflect.Kind
和reflect.Type
类型可能看起来很相似,从命名上也是,Kind和Type在英文的一些Phrase是可以互转使用的,不过在反射这块它们有挺大区别,从下面的程序中可以清楚地看到。
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
通过输出让我们清楚了两者之间的区别。reflect.Type
表示接口的实际类型,即本例中main.order
而Kind
表示类型的所属的种类,即main.order
是一个「struct」类型,类似的类型map[string]string
的Kind就该是「map」。
反射获取结构体字段的方法
我们可以通过reflect.StructField
类型的方法来获取结构体下字段的类型属性。reflect.StructField
可以通过reflect.Type
提供的下面两种方式拿到。
// 获取一个结构体内的字段数量
NumField() int
// 根据 index 获取结构体内字段的类型对象
Field(i int) StructField
// 根据字段名获取结构体内字段的类型对象
FieldByName(name string) (StructField, bool)
reflect.structField
是一个struct类型,通过它我们又能在反射里知道字段的基本类型、Tag、是否已导出等属性。
type StructField struct {
Name string
Type Type // field type
Tag StructTag // field tag string
......
}
与reflect.Type
提供的获取Field
信息的方法相对应,reflect.Value
也提供了获取Field
值的方法。
这块需要注意,不然容易迷惑。下面我们尝试一下通过反射拿到order
结构体类型的字段名和值
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
t := reflect.TypeOf(q)
if t.Kind() != reflect.Struct {
panic("unsupported argument type!")
}
v := reflect.ValueOf(q)
for i:=0; i < t.NumField(); i++ {
fmt.Println("FieldName:", t.Field(i).Name, "FiledType:", t.Field(i).Type,
"FiledValue:", v.Field(i))
}
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
运行输出:
FieldName: ordId FiledType: int FiledValue: 456
FieldName: customerId FiledType: int FiledValue: 56
除了获取结构体字段名称和值之外,还能获取结构体字段的Tag。
reflect.Value转换成实际值
现在离完成我们的SQL生成器还差最后一步,即还需要把reflect.Value
转换成实际类型的值,reflect.Value
实现了一系列Int()
, String()
,Float()
这样的方法来完成其到实际类型值的转换。
反射搞一个SQL生成器
上面我们已经了解完写这个SQL生成器函数前所有的必备知识点啦,接下来就把他们串起来,加工完成createQuery
函数。
SQL生成器完整的实现和测试代码如下:
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{}) string {
t := reflect.TypeOf(q)
v := reflect.ValueOf(q)
if v.Kind() != reflect.Struct {
panic("unsupported argument type!")
}
tableName := t.Name() // 通过结构体类型提取出SQL的表名
sql := fmt.Sprintf("INSERT INTO %s ", tableName)
columns := "("
values := "VALUES ("
for i := 0; i < v.NumField(); i++ {
// 注意reflect.Value 也实现了NumField,Kind这些方法
// 这里的v.Field(i).Kind()等价于t.Field(i).Type.Kind()
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
columns += fmt.Sprintf("%s", t.Field(i).Name)
values += fmt.Sprintf("%d", v.Field(i).Int())
} else {
columns += fmt.Sprintf(", %s", t.Field(i).Name)
values += fmt.Sprintf(", %d", v.Field(i).Int())
}
case reflect.String:
if i == 0 {
columns += fmt.Sprintf("%s", t.Field(i).Name)
values += fmt.Sprintf("'%s'", v.Field(i).String())
} else {
columns += fmt.Sprintf(", %s", t.Field(i).Name)
values += fmt.Sprintf(", '%s'", v.Field(i).String())
}
}
}
columns += "); "
values += "); "
sql += columns + values
fmt.Println(sql)
return sql
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
e := employee{
name: "Naveen",
id: 565,
address: "Coimbatore",
salary: 90000,
country: "India",
}
createQuery(e)
}
输出对应的标准SQL
插入语句:
INSERT INTO order (ordId, customerId); VALUES (456, 56);
INSERT INTO employee (name, id, address, salary, country); VALUES ('Naveen', 565, 'Coimbatore', 90000, 'India');
Clear is better than clever. Reflection is never clear.
Reflection is a very powerful and advanced concept in Go and it should be used with care. It is very difficult to write clear and maintainable code using reflection. It should be avoided wherever possible and should be used only when absolutely necessary.