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大法则:

  1. 反射可以将“接口变量”转换为“反射对象”

反射是一种检测存储在接口变量中值和类型的机制。通过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函数

全部评论

相关推荐

不愿透露姓名的神秘牛友
11-20 19:57
已编辑
某大厂 golang工程师 23.0k*16.0, 2k房补,年终大概率能拿到
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-14 17:26
已编辑
点赞 评论 收藏
分享
11-29 20:28
已编辑
重庆邮电大学 C++
米可世界 Go游戏开发 21k*15 硕士其他
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务