GoLang 反射简介

2019-04-06 language golang network

与 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 错误。

参考