44

golang反射理解

 5 years ago
source link: https://studygolang.com/articles/20365?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

golang反射理论基础

反射就是动态的获取对象的信息,动态的执行对象的方法,为什么不直接获取对象的属性呢?为什么不直接调用他的方法呢?因为有时候不知道这个对象具体是什么类型,具体有哪些属性和方法。 golang中的反射和类型息息相关,所以在了解反射之前一定要对golang的类型有一定的认识,golang的反射是对接口变量的动态类型(type)和动态值(value)相关的操作,所以golang的反射只和接口变量有关,反射是建立在接口变量基础之上的,在此可能会有这样的疑问,任何一个类型的变量都可以通过反射进行操作呀!比如int类型变量,我可以通过反射修改这个变量的值。是的,但这其实和反射只和接口变量有关并不冲突,因为接口变量也包括空接口变量,如:var i 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,该方法返回接口类型的动态值、指针指向的值。 这里说的动态类型和动态值信息是接口变量的动态类型和动态值信息,但归根结底是原始变量的类型和值信息。

反射调用原始对象方法

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

package main
​
import (
    "fmt"
    "reflect"
)
​
type User struct {
    Id    int
    Age   int
    Name  string
    Class string
}
​
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 main() {
    u := User{Id : 1, Age:12, Name:"Erin", Class: "FastTwo"}
    v := reflect.ValueOf(&u)
​
    // find method by name
    m1 := v.MethodByName("ShowInfo")
​
    if m1.IsValid() {
        p1 := []reflect.Value{}
        m1.Call(p1)
    } else {
        fmt.Println("Method ShowInfo is not found or not exported.")
    }
​
    // find method by name
    m2 := v.MethodByName("SayHi")
    if m2.IsValid() {
        p2 := []reflect.Value{
            reflect.ValueOf("PangboLee"),
        }
        m2.Call(p2)
    } else {
        fmt.Println("Method SayHi is not found or not exported.")
    }
​
​
    // find method by number
    mnums := v.NumMethod()
    fmt.Println(mnums)
    for i := 0; i< mnums; i++  {
        m := v.Method(i)
        fmt.Println(m.String())
    }
}

注意: 只有可导出的方法才可以被调用,如果方法没有被找到,直接调用会panic,所以比较安全的做法是在调用之前使用IsValid()方法进行进行检查.

反射修改原始对象属性值

通过反射可以修改原始变量的值,但是需要一定的条件支持,传入的实参必须是指针类型,我们都知道,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)
执行结果:
i =  200

遗留问题 第一个value对象的Type类型是原始变量类型的指针类型,持有的值是原始变量的指针,第二个value对象的Type类型是原始变量类型,值是原始变量值。在这里为什么不能通过第一个value对象直接修改原始对象的值呢?还需要调用它的Elem()方法?

参考:

https://blog.csdn.net/yuanjize1996/article/details/82988093

https://studygolang.com/articles/11374


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK