Golang反射入门

什么是反射?

反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。应用能够通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

其含义就是我们可以通过语言提供的反射功能,在程序运行过程中动态的调用方法、获取并改变变量属性,其核心就是其动态性。一般情况下,代码逻辑在编译时就已经确定了,变量的类型,方法调用关系都是确定的,但是有了反射,我们可以根据外部输入,动态的调整变量类型与属性、调用不同的方法。

Go语言反射

基本概念

Go 语言中通过 reflect包来实现对反射的支持,reflect包中最重要的两个方法就是 reflect.Typeofreflect.ValueOf

  • reflect.Typeof: 获取变量的类型信息,其返回的是名为Type 的接口,接口中实现了众多的方法,用来获取任意变量的类型,关于Type 接口的方法详情,可以参考:Type接口函数全解析

  • reflect.ValueOf:获取变量的类型信息,其返回的是名为Value的结构体,用来获取任意变量的值

从以上,我们还可以获取到的信息就是,Go语言中,对于一个变量,我们需要从两个维度来描述它,分别是 类型

反射对象的获取

func main() {
	a := 100
	aType := reflect.TypeOf(a) // 获取变量类型
	aValue := reflect.ValueOf(a) // 获取变量值
	fmt.Println(aType) // int
	fmt.Println(aValue) // 100
}

通过以上代码,我们就实现了对反射对象的获取,下面我们看下 reflect.TypeOfreflect.ValueOf的定义

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value 

从这两个函数的实现,我们可以看出其参数值都是interface,但是我们一般很少直接传入interface类型,都是像stringint等的具体类型,这里就涉及到变量类型的隐式转换的过程:

具体类型 -> interface类型 -> 反射类型

由于任何具体类型,都是可以赋值给反射类型的,所以从 具体类型 -> interface类型的过程中被隐藏,如下方式我们显式的来进行类型转换:

func main() {
	a := 100
	var b interface{} = a // 此过程可以通过隐式转换而忽略

	bType := reflect.TypeOf(b) // 获取变量类型
	bValue := reflect.ValueOf(b) // 获取变量值
	fmt.Println(bType) // int
	fmt.Println(bValue) // 100
}

反射类型向基础类型的转换

上面提到了具体类型反射类型的转换,那么,是否可以将反射类型基础类型进行转换呢?答案是肯定的,我们可以使用reflect.Value.Interface 方法来实现从反射类型基础类型的转换,我们先看看reflect.Value.Interface方法的定义:

func (v Value) Interface() (i interface{}) 

它返回的是 interface类型的对象,那既然上面都可以将基础类型直接转换成interface类型,那么我们能不能也直接通过如下方法,将interface类型直接赋值给基础类型,从而直接获取到基础类型呢?

func main() {
	a := 100
	var b int
	aValue := reflect.ValueOf(a) // 获取变量值
	b = aValue.Interface() // error: cannot use aValue.Interface() (type interface {}) as type int in assignment: need type assertion
	fmt.Println(b)
}

是不行的,编译器报出了cannot use aValue.Interface() (type interface {}) as type int in assignment: need type assertion的错误,其意思就是不能将 interface {} 类型转换成int类型,需要类型断言,这是由于 interface型-> 基础类型的转换必须是显式的,需要通过类型断言实现:

func main() {
	a := 100
	var b int
	aValue := reflect.ValueOf(a) // 获取变量值
	b = aValue.Interface().(int) // 类型断言
	fmt.Println(b) // 100
}

修改反射对象值

在学习这一部分之前,我们先了解下与之相关的重要方法:

  • Elem:获取反射对象指向的元素值,类似于Go语言中的* 操作,取值的对象值必须是接口或指针,否则会panic
  • Addr:对于可寻址的值,返回值的地址
  • CanAddr:判断值是否可寻址
  • CanSet:判断值是否可被设置
func main() {
	a := 100
	aValue := reflect.ValueOf(a)
	fmt.Println(aValue.CanSet()) // false
	fmt.Println(aValue.CanAddr()) // false
	fmt.Println(aValue.Elem()) // panic: reflect: call of reflect.Value.Elem on int Value
}

由于Go语言中,方法的传参都是值传递的,所以 aValue只是a的副本的反射对象,因此是不可被设置值,也是不可被取地址的,aValue.Elem()报错原因是a不是指针或接口类型,而是int类型

func main() {
	a := 100
	aValue := reflect.ValueOf(a)
    bValue := reflect.ValueOf(&a)
	fmt.Println(bValue.CanSet()) // false
	fmt.Println(bValue.CanAddr()) // false
	cValue := bValue.Elem()
	fmt.Println(cValue.CanSet()) // true
	fmt.Println(cValue.CanAddr()) // true
	fmt.Println(cValue.Addr()) // 0xc000096000
	fmt.Println(bValue) // 0xc000096000
	fmt.Println(&a) // 0xc000096000
	cValue.SetInt(200) // 修改反射对象值
	fmt.Println(cValue) // 200
	fmt.Println(aValue) // 100
	fmt.Println(a) // 200
}

bValue 只是 &a(a的地址) 的拷贝,因此也是不可被设置值、不可被取地址的,但是通过 bValue.Elem(),相当于是获取了地址所指向的值,而这个值就是a,因此是可以取地址的,从相同的地址也可以看出来。通过cValue.SetInt(200)将a的值设置为200,但是aValue的值仍为100,因此印证了其只是a的拷贝。

对于可以设置值的反射对象,可以用reflect.Value提供的SetSetXxx方法来设置新的值。

反射应用

如下代码实现了通过命令行调用对应方法的代码:

type MyFuncLib struct{}

func (m MyFuncLib) HelloWorld() {
	fmt.Println("Hello World!")
}

func (m MyFuncLib) Hello(name string) {
	fmt.Println("hello,", name)
}

func (m MyFuncLib) Hellos(names ...string) {
	for _, name := range names {
		fmt.Println("hello,", name)
	}
}

func (m MyFuncLib) Say(prefix string, names ...string) {
	for _, name := range names {
		fmt.Println(prefix, ",", name)
	}
}

func main() {
	// 参数校验,参数数量小于2 一定不合法
	if len(os.Args) < 2 {
		panic("参数太少!")
	}

	mf := MyFuncLib{}
	mfType := reflect.TypeOf(mf)
	mfValue := reflect.ValueOf(mf)

	// MethodByName: 通过名称来获取方法信息,如果 ok 为 false, 表示无法获取到方法
	if method, ok := mfType.MethodByName(os.Args[1]); ok {
		// IsVariadic:判断方法是否存在可变参数
		if method.Type.IsVariadic() {
			// 方法存在一个默认的参数 mfValue,且可变参可以传入0或多个参数
			// 故NumIn比实际必须要手动传入的参数多2
			// 参数数量需满足 NumIn() - 2 <= os.Args - 2
			if method.Type.NumIn() > len(os.Args) {
				panic("参数太少!")
			}
		} else {
			// 方法存在一个默认的参数 mfValue,故NumIn比实际需要手动传入的参数多1
			// NumIn() - 1 == os.Args - 2
			if method.Type.NumIn() != len(os.Args)-1 {
				panic("参数数量不匹配!")
			}
		}

		// 方法存在一个默认的参数 mfValue
		args := []reflect.Value{mfValue}

		// 如果存在额外的参数
		if len(os.Args) > 2 {
			for i, arg := range os.Args[2:] {
				argV := reflect.ValueOf(arg)
				argT := reflect.TypeOf(arg)

				var index int

				// 判断 当前方式是否存在变参 & 当前参数是否是变参的参数
				// 变参 一定是方法最后一个参数,如果参数数量大于等于参数总数,则一定是变参参数
				if method.Type.IsVariadic() && i+1 >= method.Type.NumIn()-1 {
					// 变参是方法最后一个参数
					index = method.Type.NumIn() - 1

					// 判断变参的值是否合法
					// Elem:对于array,Elem方法可以获取数组存储的类型,如 对于[]string 返回 string
					// ConvertibleTo:判断参数 argT 类型是否可以转换成参数类型 method.Type.In(index).Elem()
					if !argT.ConvertibleTo(method.Type.In(index).Elem()) {
						panic(fmt.Sprintf("%s can't ConvertibleTo %s", argT, method.Type.In(index).Elem()))
					}
					// Convert:将 argV 转换成 method.Type.In(index).Elem() 类型
					args = append(args, argV.Convert(method.Type.In(index).Elem()))
					continue
				}

				index = i + 1
				// 参数类型校验
				if !argT.ConvertibleTo(method.Type.In(index)) {
					panic(fmt.Sprintf("%s can't ConvertibleTo %s", argT, method.Type.In(index)))
				}

				// 转换成期望的参数类型
				args = append(args, argV.Convert(method.Type.In(index)))

			}
		}
        // 调用Call方法来实现对方法的调用
		method.Func.Call(args)
	} else {
		panic("无法找到此方法:" + os.Args[1])
	}
}

当然,上面只是反射的最基础的应用,Go语言里面的RPC就是用反射实现的,感谢趣的可以看看RPC库的代码,相信会对RPC会有更深的理解。

参考:

https://baike.baidu.com/item/%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章