Golang-反射(reflect)初探

反射是什么?

反射就是动态的获取对象的信息,动态的执行对象的方法。可以理解成一个代理,我通过reflect去代理一个对象,去拿它的信息,去执行它的方法。

golang的反射是对接口变量动态类型(type)和动态值(value)相关的操作,所以golang的反射只和接口类型变量interface{}有关。

在golang中,一个空的接口变量可以是任何类型的变量。

反射包的支持

golang中提供了reflect包对反射的支持,其中主要的有两个方法和两个struct(准确的说是一个struct和一个interface),用来分别获取和操作接口变量的动态类型和动态值。 两个方法:

  • func TypeOf(i interface{}) Type {...} 该方法接收一个空接口类型的变量,实参会被复制一份,并转换为空接口变量,最终作为TypeOf的参数。在函数内部会将接口变量的动态类型信息取出,并封装到reflect.Type返回,因为reflect.Type是一个接口,真正使用的是接口的实例,在这里真正的实例是reflect.rtype。
  • func ValueOf(i interface{}) Value {...} 与TypeOf 函数类型,接收的也是一个副本,然后转为空接口变量,在方法内部取出接口变量的动态值,然后封装为一个reflect.Value对象返回。

对应的两个类型:

  • Type 这是一个接口,真正使用的该接口的实例是reflect.rtype,该实例持有动态类型的所有信息,并且提供了很多方法让我们可以对其进行操作。

    • kind(): 获取具体类型的底层类型;
    • Elem(): 这个方法返回的是原始变量的元素的类型,注意是元素,也可以说是返回的Array、Slice、Ptr、map、channel的基类型,这些类型都有一个共同点,就是内部都有一个内部元素类型,Elem()方法返回的就是这个内部元素的类型,总结如下,如果原始变量的类型不是以上五种其中的一种,调用Elem()方法会抛出异常。 对于指针类型来说Elem的结果是它指向的数据的类型 对于数组和切片类型来说Elem的结果是它存储的元素的类型 对于Map类型来说Elem的结果是它存储的Value的类型 对于通道类型来说Elem的结果是通道可以存储的数据的类型
  • Value 这是一个struct,持有动态值的所有信息,除了动态值信息之外还可以提供了获取动态类型的信息。Value对象主要有以下几个方法:

    • Type(): Type方法返回接口变量的动态类型信息,也就是传入ValueOf方法的原始变量的类型;
    • Kind(): 与Type的kind方法一样,返回的是原始类型;
    • Interface(): 这是一个很重要也很常用的方法,我们知道reflect.ValueOf()方法接收一个空接口类型的变量,然后返回一个reflect.Value类型的对象,那么Interface()方法就和ValueOf()方法相反,他把一个reflect.Value对象还原回一个空接口类型的变量,因为reflect.Value变量本身就只有接口变量的值,并且还有接口变量的类型信息,所以还原为一个接口类型的变量是很容易的。通过这个方法获取到了接口类型变量之后,我们就可以再将接口类型变量转换为原始类型的变量,通过如下方式的类型断言:x, ok := v.Interface().(int)
    • Elem(): 调用该方法的Value对象,它的底层类型必须是接口类型或指针类型,否则会panic,该方法返回接口类型的动态值、指针指向的值。 这里说的动态类型和动态值信息是接口变量的动态类型和动态值信息,但归根结底是原始变量的类型和值信息。

反射三定律

  1. 反射可以将“接口类型变量”转换为“反射类型对象”。
  2. 反射可以将“反射类型对象”转换为“接口类型变量”。
  3. 如果要修改“反射类型对象”,其值必须是“可写的”(settable)。

反射与方法简单实例

通过反射调用原始对象的方法还是比较简单的,通过两个步骤即可完成: 首先获取原始对象的方法对象(获取到的方法也是一个对象),可以通过方法名获取,也可以通过序号进行获取; 构造方法需要的参数 直接执行方法对象的Call方法,并传入构造好的方法。 完整示例如下:

package reflecttest

import (
  "fmt"
  "reflect"
)

func Execute() {
  TestReflect()
}

type User struct {
  Id    int
  Age   int
  Name  string
  Class string
}

func (user User) _privateM() {

}

func (user User) ShowInfo() {
  fmt.Printf("Hi All, I am %s. I am %d years old. Thank you.\n", user.Name, user.Age)
}

func (user User) SayHi(name string) {
  fmt.Printf("Hello, %s !\n", name)
}

func TestReflect() {
  u := User{Id: 1, Age: 12, Name: "Erin", Class: "FastTwo"}
  v := reflect.ValueOf(&u)

  // 通过方法名返回方法
  m1 := v.MethodByName("ShowInfo")

  if m1.IsValid() { // 检查该方法是否能被访问
    p1 := []reflect.Value{}
    m1.Call(p1)
  } else {
    fmt.Println("Method ShowInfo is not found or not exported.")
  }

  // 放方法需要传入一个参数时
  m2 := v.MethodByName("SayHi")
  if m2.IsValid() {
    p2 := []reflect.Value{
      reflect.ValueOf("World"),
    }
    m2.Call(p2)
  } else {
    fmt.Println("Method SayHi is not found or not exported.")
  }

  // 私有方法,IsValid会返回true
  m3 := v.MethodByName("_privateM")
  if m3.IsValid() {
    p3 := []reflect.Value{}
    m3.Call(p3)
  } else {
    fmt.Println("Method _privateM is not found or not exported.")
  }

  // 获取所有方法
  mnums := v.NumMethod()
  fmt.Println(mnums)
  for i := 0; i < mnums; i++ {
    m := v.Method(i)
    fmt.Println(m.String())
    paramsNum := m.Type().NumIn() // 查看方法参数个数
    fmt.Printf("paramsNum: %v\n", paramsNum)
    for i := 0; i < paramsNum; i++ {
      param := m.Type().In(i) // 查看方法参数类型
      fmt.Printf("param: %v\n", param)
    }
  }
}
复制代码

反射与属性

通过反射可以修改原始变量的值,但是需要一定的条件支持,传入的实参必须是指针类型。

golang在调用方法或函数时传入的实际是实参的副本,如果传入的不是指针类型,而是具体类型的话,在方法或函数中得到的是一个原始对象的副本,在方法中对这个副本的操作其实对原始对象已经没有任何影响,所以在这里需要传入指针类型变量。

具体修改方式,首先需要通过reflect.valueOf()获取原始对象的reflect.value对象,然后调用获得的reflect.value对象的Elem()方法,调用Elem()方法之后返回的依然是一个reflect.value对象, 第一个value对象为持有原始对象的指针,它的底层类型是指针,所以可以通过Elem()方法获取到原始对象,然后通过该对象修改原始值。

i := 100
reflect.ValueOf(&i).Elem().SetInt(200)
fmt.Println("i = ", i)
复制代码

参考文章

golang反射理解

猜你喜欢

转载自juejin.im/post/7041567775193563172