Go语言中反射
反射(reflect)是指一类应用,它能够自我描述和控制。也就是说,这类应用通过某种机制实现对自己行为的描述(Self-Repreesentation)和监测(Examination),并能根据自身行为的状态和结果,调整或修改应用描述行为的状态和相关语义。
每种语言的反射模型都不同,并且有些语言根本不支持反射。Go语言实现了反射,反射机制就是在运行时动态地调用对象的属性和方法。Go语言标准包reflect就是反射相关的,并且Go语言gRPC也是通过反射实现的。
在了解反射之前,先来了解一下Go语言关于类型设计的一些原则:
变量包括类型、值两部分,理解了这一点就知道为什么nil != nil了。类型包括静态类型和具体类型。简单来说,静态类型是指在编码时看到的类型(int, string等),具体类型是指在运行时系统看见的类型。
类型的断言能否成功,取决于变量的具体类型,而不是静态类型。因此,一个只读变量如果它的具体类型也实现了可写的方法,那么它可以被断言为写入者。
反射就是建立在类型之上。Go语言指定类型的变量的类型是静态的(即int, string等这些类型的变量,他们的类型的静态类型),在创建变量的时候就已经确定。反射主要与Go语言的接口类型有关(它的类型是具体类型),只有接口类型才有反射。
Go语言的实现中,每个接口变量都有对应的一对数据,这个配对中记录了实际变量的值和类型,其形式为(value, type)。
value是实际变量的值,type是实际变量的类型。一个interface {}类型变量包含2个指针,一个指针指向值得类型(对应具体类型),一个指针指向实际的值(对应value)。
例如:创建类型为*os.File的变量,然后将其赋值给一个接口变量i
tty, _ := os.OpenFile("test.txt", os.O_RDWR, 0) var reader io.Reader reader = tty
接口变量reader中这个配对记录的信息为(tty, *os.File)。这个配对在接口变量的连续赋值过程中是不变的。将接口变量reader赋值给另外一个变量writer,如下
var writer io.Writer writer = reader.(io.Writer)
接口变量writer的这一对数据与reader的这一对数据相同,都是(tty, *os.File),即使writer是空接口类型,这一对数据也是不变的,接口及其配对的存在,是Go语言中实现反射的前提,理解了这一对数据的意思,就更容易理解反射了:反射就是用来检测存储在接口变量内部的(值、具体类型)这一对数据对的一种机制——Go语言reflect包提供了两种访问接口变量内容的方法:reflect.ValueOf()函数和reflect.TypeOf()。
reflect.ValueOf()的函数定义如下:
func ValueOf(i interface{}) Value {...}
VauleOf()函数用来获取输入参数接口中的数据的值,如果接口为空,则返回0。
reflect.TypeOf()函数的定义如下:
func TypeOf(i interface{}) Type {...}
TypeOf()函数用来动态获取输入参数接口中的值的类型,如果接口为空,则返回nil。
通过如下示例说明ValueOf()和TypeOf()函数的作用:
import ( "fmt" "reflect" ) func main() { var f float32 = 3.14 fmt.Println("type is:", reflect.TypeOf(f)) fmt.Println("value is:", reflect.ValueOf(f)) } // type is: float32 // value is: 3.14
通过以上示例可以看到,reflect.TypeOf()函数用于直接返回变量类型,如float32, string, struct等。reflect.VauleOf()函数用于返回变量具体的值。也就是说反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种。
当执行reflect.VauleOf()函数后,就得到了一个类型为reflect.Value的变量,可以通过它本身的Interface()方法获取接口变量的真实内容。然后通过类型判断进行转换,转换为原有真实类型。但是这个原有真实类型可能是已知原有类型,也有可能是未知原有类型。因此,下面分两种情况进行说明。
1. 已知原有类型(进行“强制类型转换”)。已知类型后,转换为对应的类型的做法为直接通过Interface()方法强制转换,格式如下:
realVaule := value.Interface().(已知的类型)
通过一个示例说明。
import ( "fmt" "reflect" ) func main() { var f float32 = 3.14 pointer := reflect.ValueOf(&f) value := reflect.ValueOf(f) convertPointer := pointer.Interface().(*float32) convertValue := value.Interface().(float32) fmt.Println(convertPointer) fmt.Println(convertValue) } // 0xc00001a0a8 // 3.14
在转换的时候,类型要求非常严格,如果转换的类型不完全符号,则直接引发panic。同时要区分是指针还是值,也就是说反射可以将“反射类型对象”重新转换为“接口类型变量”。
2. 未知原有类型。很多情况下,我们可能并不知道其具体类型,这时需要进行遍历探测其Filed来得知其类型:
import ( "fmt" "reflect" ) type Student struct { Id int Name string Score int } func (s Student) PrintScore() { fmt.Printf("%s's score is %d\n", s.Name, s.Score) } func GetFiledAndMethod(input interface{}) { getType := reflect.TypeOf(input) getValue := reflect.ValueOf(input) fmt.Println("Type:", getType) fmt.Println("All filed:", getValue) for i := 0; i < getType.NumField(); i++ { filed := getType.Field(i) value := getValue.Field(i).Interface() fmt.Printf("%v: %v = %v\n", filed.Name, filed.Type, value) } for i := 0; i < getType.NumMethod(); i++ { m := getType.Method(i) fmt.Printf("%v: %v\n", m.Name, m.Type) } } func main() { s := Student{1, "Jack", 96} GetFiledAndMethod(s) } // Type is: Student // All value is: {1 Jack 96} // Id: int = 1 // Name: string = Jack // Score: int = 96 // PrintScore: func(main.Student)
通过例子可以得知:
获取未知类型的interface(接口)的具体变量及其类型的步骤如下:
- 先获取interface的reflect.Type,然后通过NumFiled进行遍历
- 再通过reflect.Type的Filed获取其Field
- 最后通过Field的Interface()得到对应的value
获取未知类型的interface的所属方法(属性)的步骤如下:
- 先获取interface的reflect.Type,然后通过NumMethod进行遍历
- 再分别通过reflect.Type的Method获取对应的真实方法/函数
- 最后对结果取其Name和Type得知具体的方法名
通过reflect.Value设置实际变量的值。
reflect.Value是通过reflect.VauleOf(interface)方法获得的,只有当参数interface是指针的时候,才能通过reflect.Value修改实际变量interface的值。即要修改反射类型的对象就一定要保证其值是“可寻址的”。示例如下:
import ( "fmt" "reflect" ) func main() { var f float32 = 3.14 fmt.Println("原来的值是:", f) pointer := reflect.ValueOf(&f) newValue := pointer.Elem() fmt.Println("指针类型是:", newValue.Type()) fmt.Println("指针是否可设置", newValue.CanSet()) // 重新赋值 newValue.SetFloat(3.1415926) fmt.Println("新值是:", f) } // 原来的值是: 3.14 // 指针类型是: float32 // 指针是否可设置 true // 新值是: 3.1415925
对上例说明如下:
- 需要传入的参数是*float32这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意参数一定要是指针类型
- 如果传入的参数不是指针而是变量,通过Elem获取原始值对应的对象会触发panic
- 通过CanSet方法查询是否可以设置。可设置返回true
通过reflect.ValueOf()函数来进行方法的调用。
前面只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型,如何重新设置新值等。但是在工程应用中,有一个常用且高级的用法,就是通过reflect包来进行方法的调用。比如做框架工程时,需要可以随意扩展方法,或者说用户可以自定义方法,那么通过什么手段扩展让用户能够自定义呢?关键点在于用户的自定义方法是未知的,因此可以通过reflect包来实现。
import ( "fmt" "reflect" ) type Student struct { Id int Name string Score int } // PrintScoreWithArgs 带参数的方法 func (s Student) PrintScoreWithArgs(name string, score int) { fmt.Println("Call PrintScoreWithArgs, args:", "name:", name, "score:", score) } // PrintScoreWithoutArgs 不带参数的方法 func (s Student) PrintScoreWithoutArgs() { fmt.Println("Call PrintScoreWithoutArgs", "name:", s.Name, "score:", s.Score) } func main() { s := Student{1, "Anton", 96} getValue := reflect.ValueOf(s) // 调用带参数的方法 methodValue := getValue.MethodByName("PrintScoreWithArgs") args := []reflect.Value{reflect.ValueOf("Jack"), reflect.ValueOf(95)} methodValue.Call(args) // 调用不带参数的方法 methodValue = getValue.MethodByName("PrintScoreWithoutArgs") args = make([]reflect.Value, 0) methodValue.Call(args) } // Call PrintScoreWithArgs, args: name: Jack score: 95 // Call PrintScoreWithoutArgs name: Anton score: 96
如果要通过反射调用,那么首先要将方法注册,也就是通过MethodByName()方法获取方法值methodValue对象,然后通过反射调用methodVaule对象的Call()方法。
Go语言的反射比较慢,这个和它的API设计有关。Go语言的反射设计如下:
type_ := reflect.TypeOf(obj) field, _ := type_.FieldByName("name")
这里的field对象是reflect.StructField类型,但是它没有办法用于读取对应对象上的值。如果要读取值,则需要调用另外一套对象,而不是type的反射:
type_ := reflect.ValueOf(obj) filedValue := type_.FieldByName("name")
这里读取的fieldValue类型是reflect.Value,它是一个具体值,不是一个可复用的反射对象。每次反射都要分配请求的内存并返回指向它的指针给这个reflect.Value结构体,并且还涉及垃圾回收。
综上,Go语言reflect慢的原因为:
- 频繁进行内存分配以及后续的GC
- reflect反射实现里边有大量的枚举、类型转换、for循环
反射3大法则:
- 反射可以将“接口变量”转换为“反射对象”
反射是一种检测存储在接口变量中值和类型的机制。通过reflect包的一些函数,可以把接口转换为反射定义的对象。reflect包常用的函数如下:
- reflect.ValueOf(i any) Value: 获取变量的值,值是通过reflect.Value对象描述的
- reflect.TypeOf(i any) Type:获取变量的静态类型,值是通过reflect.Type对象描述的,可以通过fmt.Println打印
- reflect.Value.Kind() Kind:获取变量的底层类型,底层类型可能是int, float, string, struct, slice等
- reflect.Value.Type() Type:获取变量值的底层类型,等同于reflect.TypeOf()。
2. 反射可以将“反射对象”转换为“接口变量”
根据一个reflect.Value类型变量,可以使用Interface()函数恢复其接口类型的值。Interface方法会把value和type信息打包并填充到一个接口变量中,然后返回。Interface()方法声明如下:
func (v Vaule) Interface() interface{}
可以通过类型断言恢复底层的具体值:
value := v.Interface().(int)
Interface()方法就是用来实现将反射对象转换成接口变量的一个桥梁。如果Vaule是结构体的非导出字段,调用该函数会引发panic。
3. 如果要修改“反射对象”,其值必须是“可写的”(settable)
当使用reflect.TypeOf和reflect.ValueOf时,如果传递的不是接口变量的指针,反射环境里的变量值始终只是真实环境里的一个副本,对该反射对象进行修改,并不能反映到真实环境里。在反射的规则里,需要注意以下几点:
- 不是接收变量指针创建的反射对象,是不具备“可写性”的
- 是否具备“可写性”,可通过CanSet()函数来确定
- 对不具备“可写性”的对象进行修改是没有意义的,也是不合法的,因此会报错
如果要让反射对象具备“可写性”,需要注意以下几点:
- 创建反射对象时传入变量的指针
- 使用Elem()函数返回指针指向的数据
import ( "fmt" "reflect" ) func main() { var f float32 = 3.14 v1 := reflect.ValueOf(f) fmt.Println(v1.CanSet()) p := reflect.ValueOf(&f) fmt.Println(p.CanSet()) v2 := p.Elem() fmt.Println(v2.CanSet()) v2.SetFloat(3.1415926) fmt.Println(f) } // false // false // true // 3.1415925
可以通过Value.Kind()函数获取对象底层类型,然后根据获取到的类型选择对应的Value.SetXxx函数