gocookbook icon indicating copy to clipboard operation
gocookbook copied to clipboard

Go反射的使用教程

Open kevinyan815 opened this issue 3 years ago • 0 comments

内容节选自我的原创文章用手写一个工具的过程讲清楚Go反射的使用方法和应用场景

Go语言的反射包

Go语言自带的reflect包实现了在运行时进行反射的功能,这个包可以帮助识别一个interface{}类型变量其底层的具体类型和值。我们的createQuery函数接收到一个interface{}类型的实参后,需要根据这个实参的底层类型和值去创建并返回INSERT语句,这正是反射包的作用所在。

在开始编写我们的通用SQL生成器函数之前,我们需要先了解一下reflect包中我们会用到的几个类型和方法,接下来我们先逐个学习一下。

reflect.Type 和 reflect.Value

经过反射后interface{}类型的变量的底层具体类型由reflect.Type表示,底层值由reflect.Value表示。reflect包里有两个函数reflect.TypeOf()reflect.ValueOf() 分别能将interface{}类型的变量转换为reflect.Typereflect.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.Typeofreflect.Valueof 函数的调用。从输出,我们可以看到程序输出了interface{}类型实参对应的底层具体类型和值。

Go语言反射的三法则

这里插播一下反射的三法则,他们是:

  1. 从接口值可以反射出反射对象。
  2. 从反射对象可反射出接口值。
  3. 要修改反射对象,其值必须可设置。

反射的第一条法则是,我们能够吧Go中的接口类型变量转换成反射对象,上面提到的reflect.TypeOfreflect.ValueOf 就是完成的这种转换。第二条指的是我们能把反射类型的变量再转换回到接口类型,最后一条则是与反射值是否可以被更改有关。三法则详细的说明可以去看看德莱文大神写的文章 Go反射的实现原理,文章开头就有对三法则说明的图解,再次膜拜。

下面我们接着继续了解完成我们的SQL生成器需要的反射知识。

reflect.Kind

reflect包中还有一个非常重要的类型,reflect.Kind

reflect.Kindreflect.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.orderKind表示类型的所属的种类,即main.order是一个「struct」类型,类似的类型map[string]string的Kind就该是「map」。

注意除了reflect.Type外reflect.Value类型也实现了Kind方法返回reflect.Kind对象,这个不要迷惑到(其实NumField方法也是,Type和Value两个类型都实现的方法,应该是为了方便减少使用反射时的代码量)

反射获取结构体字段的方法

我们可以通过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值的方法。

func (v Value) Field(i int) Value {
...
}

func (v Value) FieldByName(name string) Value {
...
}

这块需要注意,不然容易迷惑。下面我们尝试一下通过反射拿到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'); 

kevinyan815 avatar Oct 26 '21 06:10 kevinyan815