Go微服务实战
上QQ阅读APP看书,第一时间看更新

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类型,这容易让程序崩溃。

基于以上原因,建议读者在使用反射时一定要谨慎。