与 C/C++ 不同,GoLang 的指针是不支持指针运算和转换,GoLang 是静态语言,所有变量必须标明其类型,不同类型变量不能执行赋值、比较、计算等操作;指针也有其对应的类型,在编译阶段同样会进行检查。
同时,GoLang 在运行阶段提供了一定的灵活性。
简介
在 GoLang 中 Reflect 是一个很关键的包,可以在代码运行的阶段操作任意对象,是元编程 (MetaPrograming) 的基础,但是也很容易混淆。
元编程通常操作其它 (含自身) 程序的数据,在运行阶段完成本来是在编译阶段完成的工作,从而使一些修改无需重新编译,一般需要通过反射完成。
基本概念
GoLang 是静态类型,每个变量在编译阶段已经确定了其类型,例如如下示例。
type MyInt int
var i int
var j MyInt
其中变量 i j 是不同的静态类型,虽然两者的底层数据类型是相同的,但是如果不经过转换是无法直接赋值的。
比较特殊的是接口,它定义了一系列的函数,一个接口变量可以保存任意类型的变量,只要改变量实现了接口定义的方法即可。
基本示例
变量包括了 (type, value) 两部分,分别代表了变量的类型及其值。
在 GoLang 的标准库中定义了 type Value struct 以及 type Type interface 两个类型,对应了源码中的 src/reflect/{value.go,type.go} 文件,所有的反射操作都是基于这两个类型进行的,分别可以通过 reflect.ValueOf() 以及 reflect.TypeOf() 两个函数获取,函数声明如下。
func ValueOf(i interface{}) Value
func TypeOf(i interface{}) Type
对于基本类型的操作示例如下。
package main
import (
"fmt"
"reflect"
)
func main() {
var pi float64 = 3.1415926
fmt.Println(reflect.TypeOf(pi), reflect.ValueOf(pi))
}
法则
反射作为元编程的一种方式,可以减少重复代码提高灵活度,但同时是一把双刃剑,过量使用会使程序逻辑变得复杂难以维护。同时在使用时,需要遵循三大规则。
- 从
interface{}变量可以反射出反射对象reflect.Value; - 从反射对象
reflect.Value可以获取interface{}变量; - 要修改反射对象,其值必须可设置;
法则 <1>
也就是可以从 空接口 变量获取到反射对象,虽然有些基本变量可以直接调用,例如 int string float64 等,实际上在调用 ValueOf() 或者 TypeOf() 时,因为入参类型为 interface{} ,从而隐含了类型转换。
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
实际上,这一规则在源码中有很好的体现,也解释了为什么不能是非空的接口变量。
其中有 Kind() 函数需要关注,返回的是底层的类型,而 Type() 则是指定的类型,例如。
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
fmt.Println(v.Type(), v.Kind())
会输出 main.MyInt int 。
法则 <2>
实际上就是第一条的逆向操作,可以通过 Interface() 接口可以获取到 interface{} ,实际上就是将值以及类型重新封装,然后获取结果后可以根据具体的类型进行转换。
package main
import (
"fmt"
"reflect"
)
func main() {
var pi float64 = 3.1415926
v := reflect.ValueOf(pi)
npi := v.Interface().(float64)
fmt.Printf("got value %f %f\n", npi, v.Interface())
}
在转换之后,因为是空接口类型,需要手动转换到具体的类型。
另外,一些公共函数的入参是空类型,那么实际上可以直接使用 v.Interface() 返回的值即可,无需再手动转换。
法则 <3>
是否可以设置是 reflect.Value 的一个特性 (可以通过 v.CanSet() 检查),例如如下的示例就会报错。
var pi float64 = 3.1415926
v := reflect.ValueOf(pi)
v.SetFloat(3.14)
报错的内容为 panic: reflect: reflect.Value.SetFloat using unaddressable value ,报错信息有些歧义,实际是因为 v 不能设置。
Settability 有点类似于 Addressability ,通过判断映射的对象是否包含 (或者可以操作) 原始值来判定的,例如上述的 float64 ,因为 Go 的函数调用是传值,所以在转换为 Value 之后实际上是原始值的副本。
如果直接修改副本,那么实际对原始值没有任何的影响,而且没有任何的意义,所以 Go 直接禁用了这一场景。
所以,这里在通过 ValueOf() 获取 Value 对象时,应该使用指针。
var pi float64 = 3.1415926
v := reflect.ValueOf(&pi)
v.Elem().SetFloat(3.14)
fmt.Println(pi)
注意,如果不调用 Elem() ,指针对象仍然不可以设置,上述操作相当于如下。
pi := 3.1415926
p := &pi
*p = 3.14
示例
调用函数
在 GoLang 中,函数可以像 int float 类型的变量那样赋值给某个变量,然后再调用的。
package main
import "fmt"
func hello() {
fmt.Println("Hello world!")
}
func main() {
f := hello
f()
}
函数和其它的变量一样,不过其反射的类型是 reflect.Func ,如果要调用该函数,可以通过 Call() 方法实现。
不过需要注意的是,Call() 方法的入参是 reflect.Value 类型的 slice ,返回值与之相同。
package main
import (
"fmt"
"reflect"
"strconv"
)
func Format(v int) string {
fmt.Println("Value is", v)
return strconv.Itoa(v)
}
func main() {
fv := reflect.ValueOf(Format)
r := fv.Call([]reflect.Value{reflect.ValueOf(20)}) // fv.Call(nil)
fmt.Println("Result is", r[0].Interface().(string))
//fmt.Println("Result is", r[0].String())
}
接着看下结构体中如何调用函数,与上面的区别是,需要通过 Method() 或者 MethodByName() 获取对应的函数。
package main
import (
"fmt"
"reflect"
)
type Rectangle struct {
Width, Height float64
}
func (r *Rectangle) Area() float64 {
return r.Height * r.Width
}
func (r *Rectangle) Update(w, h float64) {
r.Width = w
r.Height = h
}
func (r *Rectangle) String() string {
return fmt.Sprintf("width %g height %g", r.Width, r.Height)
}
func main() {
rect := &Rectangle{Width: 10, Height: 20}
e := reflect.ValueOf(&rect).Elem()
fmt.Println(e.MethodByName("String").Call(nil)[0])
//fmt.Println(e.Method(1).Call(nil)[0])
e.MethodByName("Update").Call([]reflect.Value{reflect.ValueOf(20.0), reflect.ValueOf(40.0)})
//e.Method(2).Call([]reflect.Value{reflect.ValueOf(20.0), reflect.ValueOf(40.0)})
fmt.Println(e.MethodByName("String").Call(nil)[0])
}
注意 Method() 函数中的顺序是函数名排序后的结果。另外,如果 rect 变量不是指针,那么就不用再调用 Elem() 方法。
func main() {
rect := Rectangle{Width: 10, Height: 20}
e := reflect.ValueOf(&rect)
fmt.Println(e.MethodByName("String").Call(nil)[0])
}
这里需要注意的是,上面的 reflect.ValueOf(&rect) 必须要取地址,否则返回的是一个结构体,那么不会支持类似 MethodByName() 这种操作。
实际上还隐藏了一个操作,也就是将 &rect 赋值给了一个接口变量,并返回给了变量 e 。
其它
如果结构体中是私有成员,那么即使通过反射也无法获取,会报 reflect.Value.Interface: cannot return value obtained from unexported field or method 错误。