4.4 反射
反射(reflect)是Go语言提供的动态获取对象类型及结构信息的方式,通过reflect可以使用Go语言提供的相关能力。
为什么需要反射呢?想象一下,如果现在要获取一个表的所有字段,并写入一个struct,当然可以一个字段一个字段地对应着写(具体可以参考7.4节),但是也可以使用select * 的方式,然后再通过反射的方式获取所有字段的类型、结果信息,最后再进行绑定。这个步骤是通用的,可以写成一个专门的包,事实上GitHub上已经有类似的包,比如sqlx。类似的情况都需要使用反射。越是需要编写尽可能通用的代码时,越是需要使用反射。
说明
反射可帮助处理未知类型,非常灵活,但是在实际编程中使用的次数比函数、方法、接口要少很多,这是因为反射的实现比较复杂,编程当中使用时要慎重。
reflect包有两个核心类型:reflect.Value和reflect.Type。前者用于存储任意值,后者用于存储任意类型。
下面通过示例代码看一下反射的用法:
book/ch04/4.4/main.go
1. package main
2.
3. import (
4. "fmt"
5. "reflect"
6. )
7.
8. type X struct {
9. A1 int
10. B1 float64
11. C1 bool
12. }
13.
14. type Y struct {
15. A2 int
16. B2 int
17. C2 float64
18. D2 string
19. }
20.
21. func main() {
22. x1 := X{A1:100,B1:3.14,C1:true}
23. y1 := Y{A2:1,B2:2,C2:1.5,D2:"hello"}
24. rx1 := reflect.ValueOf(&x1).Elem()
25. ry1 := reflect.ValueOf(&y1).Elem()
26. x1Type := rx1.Type()
27. y1Type := ry1.Type()
28. fmt.Printf("This type is %s,%d fileds of it are:\n",x1Type,rx1.NumField())
29. for i:=0;i<rx1.NumField();i++{
30. fmt.Printf("Name:%s,Type:%s,Value:%v\n",x1Type.Field(i).Name,rx1.
Field(i).Type(),rx1.Field(i).Interface())
31. }
32.
33. fmt.Printf("This type is %s,%d fields of it are:\n",y1Type,ry1.NumField())
34. for i:=0;i<ry1.NumField();i++{
35. fmt.Printf("Name:%s,Type:%s,Value:%v\n",y1Type.Field(i).Name,ry1.
Field(i).Type(),ry1.Field(i).Interface())
36. }
37. }
38. //以下是程序执行结果
39. This type is main.X,3 fileds of it are:
40. Name:A1,Type:int,Value:100
41. Name:B1,Type:float64,Value:3.14
42. Name:C1,Type:bool,Value:true
43. This type is main.Y,4 fields of it are:
44. Name:A2,Type:int,Value:1
45. Name:B2,Type:int,Value:2
46. Name:C2,Type:float64,Value:1.5
47. Name:D2,Type:string,Value:hello
第8行至第19行,定义了两个结构体:X和Y,并给这两个结构体定义了几个不同类型的成员。
第22行至第23行,定义了X和Y结构体的两个变量:x1和y1。
第24行至第25行,通过reflect.ValueOf方法获取新创建的变量的地址,一般该方法返回的是传入变量的一份值复制。此处直接传递变量的地址,得到的也是变量的地址对象。然后调用Elem方法获取地址指针指向的值封装。
第26行至第27行,通过调用Type方法可以获取变量的类型。
第28行至第37行,分别对两个变量的结构体明细字段进行打印,可以结合第39行至第47行的打印结果查看。第28行的NumField方法返回reflect.Value结构中的字段个数,而第30行中的Field函数返回的是结构中指定的字段。注意Interface函数是以接口类型返回reflect.Value结构中的字段值的,使用这个方法时要注意结构中的成员定义首字母都要大写,也就是包外可见,否则会报错。
反射的功能相对比较简单,只要熟悉reflect.Value和reflect.Type,就可以完成较为复杂的功能,再来看一个比较复杂的示例:
book/ch04/4.4/adv/main.go
1. package main
2.
3. import (
4. "fmt"
5. "reflect"
6. )
7.
8. type X struct {
9. I int
10. F float64
11. S string
12. }
13. type Person struct {
14. Name string `json:"jname"`
15. Gender int `json:"jgender"`
16. Age int `json:"jage"`
17. }
18.
19. func (x X) CompareStr(xx X) bool {
20. rx1 := reflect.ValueOf(&x).Elem()
21. rx2 := reflect.ValueOf(&xx).Elem()
22. for i:=0;i<rx1.NumField();i++{
23. if rx1.Field(i).Interface() != rx2.Field(i).Interface(){
24. return false
25. }
26. }
27. return true
28. }
29.
30. func (p Person) PrintTags() {
31. for i :=0;i<reflect.TypeOf(p).NumField();i++{
32. fmt.Println(reflect.TypeOf(p).Field(i).Tag.Get("json"))
33. }
34. }
35.
36. func main() {
37. x1 := X{I:1,F:1.2,S:"hello"}
38. x2 := X{I:1,F:1.2,S:"hello"}
39. fmt.Println(x1.CompareStr(x2))
40.
41. p := Person{Name:"Scott",Gender:1,Age:30}
42. p.PrintTags()
43.
44. }
45.
46. //以下是程序执行结果
47. true
48. jname
49. jgender
50. jage
第8行至第12行,定义了一个简单的结构体,为后续的结构体比较方法CompareStr做准备。
第19行至第28行,CompareStr方法用于比较两个X型结构体是否相等,方法的实现方式就是按照顺序比较两个字段的值,如果有一个不相等则返回false,否则最后返回true。
第37行至第39行,先定义了两个成员值完全一样的X型变量:x1和x2,然后调用了CompareStr方法,可以看到第47行打印的结果为true。
第13行至第17行,定义了一个Person结构体,每个属性都对应地定义了标签,主要是为后续PrintTags方法做准备。
第30行至第34行,定义了PrintTags方法,该方法会循环struct内的所有字段,然后查找对应的tag并打印。
第41行定义了一个Person类型的变量,并且在第42行调用了PrintTags方法。结合第48行至第50行的结果,可以看到确实打印了每个成员的标签。
反射的用法就介绍这么多,此处还是要强调反射的三个缺点:
▪反射代码的写法可读性比较差,不利于后续的运维。
▪反射的实现比较复杂,所以反射执行得比较慢,会影响程序的整体性能。
▪反射的错误在编译时无法发现,到运行时才报错,而且都是panic类型,这容易让程序崩溃。
基于以上原因,建议读者在使用反射时一定要谨慎。