什么是反射?
反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。应用能够通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
其含义就是我们可以通过语言提供的反射功能,在程序运行过程中动态的调用方法、获取并改变变量属性,其核心就是其动态性。一般情况下,代码逻辑在编译时就已经确定了,变量的类型,方法调用关系都是确定的,但是有了反射,我们可以根据外部输入,动态的调整变量类型与属性、调用不同的方法。
Go语言反射
基本概念
Go 语言中通过 reflect
包来实现对反射的支持,reflect包中最重要的两个方法就是 reflect.Typeof
与 reflect.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.TypeOf
与 reflect.ValueOf
的定义
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
从这两个函数的实现,我们可以看出其参数值都是interface
,但是我们一般很少直接传入interface类型,都是像string
,int
等的具体类型,这里就涉及到变量类型的隐式转换的过程:
具体类型 -> 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
提供的Set
或 SetXxx
方法来设置新的值。
反射应用
如下代码实现了通过命令行调用对应方法的代码:
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/