Go中的反射
反射的概念
反射是指动态语言中在程序运行时能够动态地拿到任意类型的信息、创建类型实例以及调用函数和方法。一般来说js、python这类是动态语言,而c/c++和Java以及Go是静态语言,但Java和Go却能使用反射来实现动态语言的一些特性,实现原理是其在运行时在内存中保留了类信息。
反射的优缺点
- 优点:也就是反射能实现的功能,也即动态性
- 缺点:
-
- 由于反射是在运行时执行的,因此无法在编译期检查语法异常
-
- 反射的代码是动态执行的,会加大程序的复杂度
-
- 反射需要额外的开销,会降低程序的性能
-
反射的应用
由于反射是用来实现动态读写和调用功能的,一般在一个程序内需要动态实现的是配置选项,程序通过文本配置,将文本转换成对应的类型供程序动态调用。Java中最经典的反射要属Spring框架中各种对象的创建以及ioc和aop的实现了,其中ioc的实现依赖于反射根据接口的继承实现关系和配置动态地创建实现类的实例,而aop的实现依赖于在代理类中反射调用目标类的方法。
Go的反射
Go反射的实现原理
跟Java对比,Go更是一个静态语言,因为Java是编译成字节码,字节码直接在jvm上运行,字节码中还能保留一些类型和函数信息,而Go是直接编译成二进制文件提供给操作系统调用。
Go反射的实现完全依赖于类型转换,Go的反射入口是reflect#TypeOf和reflect#ValueOf,前者返回一个Type类型的实例,用来访问类型信息;后者返回一个Value类型的实例,用来进行set/get/invoke操作,而Type和Value这两个类型都依赖于emptyInterface这个struct来实现。
看源码不难得知,TypeOf和ValueOf的形参都是interface{}类型的,因此这时就将原始类型转换成了interface{}类型,再对interface{}类型的变量取地址得到指针并强转成emptyInterface类型,emptyInterface这个struct就实现了Type接口,在其中保存了类信息。至于为什么能强转,这依赖于Go的struct是值类型的,其表示一块内存,struct的每个字段实际上表示的是相对于struct内存起始地址的offset,因此只要两个struct的内存布局相同,即可完全强转。强转后访问也是安全的,因为对struct字段的访问最终其实就是内存的访问。
Go反射的基本使用
前面提供,反射的主要作用就是访问类型的信息、动态创建类型的实例、set/get/invoke操作,以及判断两个类型的关系(比如判断一个struct是否实现了一个interface),下面对这些功能一一进行举例:
- 访问类型信息
前面提供类型信息是保存在Type类型中的,因此需要使用reflect#TypeOf先获取到一个Type类型的实例,或者使用reflect#ValueOf先获取到一个Value类型的实例,再用Value#Type函数获取到Type类型的实例。
func TypeInfo() {
obj := stru.Student{}
t := reflect.TypeOf(obj)
fields := make([]reflect.StructField, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
fields = append(fields, t.Field(i))
}
methods := make([]reflect.Method, 0, t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
methods = append(methods, t.Method(i))
}
fmt.Printf("%+v, %+v, %+v, %+v\n", t.Name(), t.Kind(), fields, methods)
}
跟Java一样,类型的字段和方法都是一个结构体来表示的,分别是StructField和Method,通过这两个结构体,我们就能了解到更多信息,比如字段的类型,方法的签名等
- 动态创建类型实例
Go反射动态创建实例使用reflect#New(Type) Value函数,这个函数的入参是一个Type类型的实例,而返回的是Value,但这个Value是返回一个指针类型的Value,使用Value#Interface返回真实类型实例的变量时,在强转时需要强转成指针类型
func ConstructValue() {
obj := stru.Student{}
t := reflect.TypeOf(obj)
newObj := reflect.New(t).Interface().(*stru.Student)
oo := stru.Student{
Name: "xiaoming",
}
newObjVal := reflect.NewAt(t, unsafe.Pointer(&oo)).Interface().(*stru.Student)
newObj.Name = "xiaogang"
newObjVal.Name = "xiaohong"
fmt.Printf("%+v, %+v, %+v\n", newObj, newObjVal, oo)
}
上面的代码使用reflect#New和reflect#NewAt,二者的区别是NewAt需要多传一个unsafe#Pointer类型的参数,这个指针在调用后会指向新创建的实例
- get/set/invoke
func GetSetInvoke() {
var obj inface.Eatable = &stru.Student{Name: "xiaoming"}
v := reflect.ValueOf(obj)
fmt.Println(v.Elem().FieldByName("Name").Interface())
newObj := stru.Student{Name: "xiaohong"}
v.Elem().Set(reflect.ValueOf(newObj))
fmt.Printf("%+v\n", obj)
v.Elem().FieldByName("Name").SetString("xiaogang")
fmt.Printf("%+v\n", obj)
v.MethodByName("Eat").Call([]reflect.Value{reflect.ValueOf("apple")})
}
- 判断类型之间的关系
由于Go的结构体struct类型不具备面向对象语言中的继承能力,因此也就不存在判断两个struct类型之间的继承关系,但我们还是可以判断一个struct和另一个interface之间的实现关系。
Go的Type类型提供了Implements和AssignableTo以及ConvertibleTo这三个函数来判断两上Type之间的关系,其中t1.Implements(t2)判断t1是否实现了接口t2,而t1.AssignableTo(t2)判断t1类型的变量是否可以赋值给t2类型的变量,t1.ConvertibleTo(t2)判断t1类型的变量是否可以强转成t2类型的变量。
type A struct {
Name string
}
type B struct {
Name string
}
func JudgeType() {
obj := &stru.Student{Name: "xiaoming"}
t1 := reflect.TypeOf(obj)
t2 := reflect.TypeOf((*inface.Eatable)(nil)).Elem()
fmt.Println(t1.Implements(t2))
fmt.Println(t1.AssignableTo(t2))
fmt.Println(reflect.TypeOf([]byte{}).ConvertibleTo(reflect.TypeOf("")))
aType := reflect.TypeOf((*A)(nil))
bType := reflect.TypeOf((*B)(nil))
fmt.Println(aType.AssignableTo(bType))
fmt.Println(aType.ConvertibleTo(bType))
}
上面的代码打印结果为true、true、true、false、true
这说明AssignableTo和ConvertibleTo还是有区别的,区别在于Go对变量强转的限制上,两个struct如果"布局"完全相同,那么其是可以相互强转的,但却不能相互赋值。例如上面的A和B两个struct,它们的"布局"都是只有一个string的Name字段,这说明它们的内存布局是完全一样的,因此能够安全地进行类型强转
最后Type还提供了一个Comparable函数来判断一个类型是否是可比较的,也就是这种类型的变量是否可以使用==或者当作map的key使用