golang 反射入门

44 阅读6分钟

一、什么是反射

1·定义

In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior

--维基百科

反射是程序在运行时(runtime)检查进程、内省、自我修改的一种能力。

  • 内省: 程序检查自身结构的一种能力,在运行时查询对象的值、元数据、属性和函数。
  • 自我修改: 程序修改自身结构的一种能力,程序可以在运行时对自己进行适当的修改,能够在运行时修改自己的源代码或其他程序的源代码的程序称为元程序,因而反射是元编程的一种形式。

下图是反射的体系编程系统剖析,包括一个程序和关联的数据,以及一个执行程序的解释器,解释器本身由一组系统内核上的解释指令组成。每一层解释指令都包括程序和数据。

image.png

应用程序可以通过一下几种方式改变程序本身未来的运行方式,即反射的实现方式:

  1. 更新关联数据(case 1)
  1. 改变本身的代码(case 2)
  1. 更新解释权某一级的关联数据(case 3)
  1. 更新解释器某一级的程序代码(case 4)

反射一种很灵活而强大的机制,但它也可能会破坏数据的完整性,在持久化编程系统中是不可接受的。因为这个原因,反射一般会包括类型检查,保证反射生成的代码在执行的时候是类型安全的。

回到golang,golang本身一种强类型的语言,每个变量都有一个静态类型,在编译的时候每个变量一定有确定的类型,因而go的反射的建立在类型系统之上是很自然的。

二、golang的反射基础理论

反射的实现

下图是golang反射系统概要展示,golang将对象和对象的反射分开,通过反射包reflect单独实现了一套反射API,在实现上符合封装原则,显得更加简洁。

image.png

reflect包定义了两个实现反射的重要类型:reflect.Type和reflect.Value。对象包括对象的类型和对象的值,reflect.Type存储对象的类型信息,包括对象的种类(int、slice、func...)、拥有的方法等,reflect.Value存储对象具体的值。

  • reflect.Type接口类型,因为类型信息是只读的,不能动态的修改,且不同类型的定义也不相同,使用接口可以更好的抽象,reflect.Type包含该类型所有的信息。
  • reflect.Value是一个结构体类型,reflect.Value可以获取类型信息并且对数据执行操作。
type Type interface {
    ...
}

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag  // 一些描述信息
}

reflect.TypeOf()接受一个空接口类型的参数,并返回一个reflect.Type。

func TypeOf(i interface{}) Type {
   eface := *(*emptyInterface)(unsafe.Pointer(&i))
   return toType(eface.typ)
}

reflect.ValueOf()接受一个空接口类型的参数,返回一个reflect.Value。

func ValueOf(i interface{}) Value {
   if i == nil {
      return Value{}
   }
   // 将变量保存一份副本到堆上
   escapes(i)
   return unpackEface(i)
}

空接口类型interface{}不含任何方法,因此任何变量都可以赋值给interface{},是golang中一个重要的底层抽象。interface{}的结构如下所示,由类型和数据值组成

image.png

反射三定律

  1. 反射第一定律:接口类型变量可以变成反射对象

接口类型变量分别调用reflect.TypeOf()和reflect.ValueOf()方法可以得到两种类型的反射对象。

暂时无法在飞书文档外展示此内容

image.png 2. 反射第二定律: 反射对象可以变成接口类型变量

reflect.Value通过调用Interface() 方法,可以从反射对象变为接口类型的变量。

image.png

func (v Value) Interface() (i interface{}) {
        return valueInterface(v, true)
}
  1. 反射第三定律: 可以通过可寻址的反射对象修改被对象对象的值

如果反射对象是对被反射对象的地址反射产生的,则可以对被反射对象的值进行修改。

可寻址:

  • slice的元素
  • 可寻址数组的元素
  • 可寻址struct的字段
  • 指针引用的结果

image.png 将反射三定律总结成一个图,如下所示。反射三定律告诉我们golang反射的使用方式。

image.png

三、实践

上面简单介绍了反射是什么和golang的反射,接下来就简单讲一下反射的使用。

  1. 反射第一定律举例

遍历结构体字段的名字

type Animal struct {
   Name     string `json: name `
   Age      int    `json: age `
   phone    string
}

func TestPrintField(t *testing.T) {
   printFiled := func (object interface{})  {
      // 获取反射对象
      typ := reflect.TypeOf(object)
      // 此时获得是指针的反射对象,因此,要获取指针指向对象的反射对象
      typ = typ.Elem()
      // typ.NumField() 获得结构体字段的个数
      for i := 0; i < typ.NumField(); i++ {
      // 取出第i个属性
      f := typ.Field(i)
      fmt.Printf( 字段名字:%5s, 字段类型:%6s, 字段tag:%5s\n , f.Name, f.Type, f.Tag)
      }
   }

   animal := Animal{
      Name:   Dog ,
      Age:   11,
      phone:  123456 ,
   }
   // 打印函数
   printFiled(&animal)

}

打印结果如下。通过reflect.TypeOf()方法,构造了对象animal的反射对象,成功打印出了对象animal的字段名、字段类型和字段的tag。比如在json序列化的时候,就会利用反射拿到对象的字段tag,获得序列化后的字段名。

字段名字: Name, 字段类型:string, 字段tag:json: name 
字段名字:  Age, 字段类型:   int, 字段tag:json: age 
字段名字:phone, 字段类型:string, 字段tag:json: phone 

上一个例子只获取了对象animal的类型信息,没有打印animal的值,那如何获取animal的值?

我们把上面的方法做了一个简单的修改,使用reflect.ValueOf()获取反射对象,此时,就可以打印出具体的值。

func TestPrintValue(t *testing.T) {
   printValue := func (object interface{})  {
      // 获取反射对象
      v := reflect.ValueOf(object)
      // 因为我们传入的是指针对象,所以取出指针里面内容
      v = v.Elem()
      // typ.NumField() 获得结构体字段的个数
      for i := 0; i < v.NumField(); i++ {
         // 取出第i个属性
         f := v.Field(i)
         fmt.Printf( 字段名字:%12s, 字段种类:%6s\n , f.String(), f.Kind())
      }
   }

   animal := Animal{
      Name:   Dog ,
      Age:   11,
      phone:  123456 ,
   }
   // 打印函数
   printValue(&animal)
}

打印的结果如下。可以看到Animal的Name是Dog,phone是“123456”,但是Age没有打印具体值,这是为什么?因为代码中是简单的调用了f.String(),只有字段类型为string才会打印字段的值,那如何打印Age的值咧?

字段名字:         Dog, 字段种类:string
字段名字: <int Value>, 字段种类:   int
字段名字:      123456, 字段种类:string
  1. 反射第二定律举例

打印Age的值,可以通过反射第二定律实现,将代码进行简单的修改,如果字段类型是int,先将字段Age变成接口变量,然后断言成int,就可以打印出Age的值了。

func TestReflect2Object(t *testing.T)  {
   printValue := func (object interface{})  {
      // 获取反射对象
      v := reflect.ValueOf(object)
      // 因为我们传入的是指针对象,所以取出指针里面内容
      v = v.Elem()
      // typ.NumField() 获得结构体字段的个数
      for i := 0; i < v.NumField(); i++ {
         // 取出第i个属性
         f := v.Field(i)
         switch f.Kind() {
         case reflect.Int:
            fmt.Printf( 字段名字:%12d, 字段种类:%6s\n , f.Interface().(int), f.Kind())
         default:
            fmt.Printf( 字段名字:%12s, 字段种类:%6s\n , f.String(), f.Kind())
         }
      }
   }

   animal := Animal{
      Name:   Dog ,
      Age:   11,
      phone:  123456 ,
   }
   // 打印函数
   printValue(&animal)
}

打印结果。可以观察到Age的值已经打印成功。

字段名字:         Dog, 字段种类:string
字段名字:          11, 字段种类:   int
字段名字:      123456, 字段种类:string
  1. 反射第三定律举例

如果需要修改对象Animal的值,那怎么办,这个时候我们对代码再进行一点点修改,

func TestChangeObjectSuccess(t *testing.T)  {
   printValue := func (object interface{})  {
      // 获取反射对象
      v := reflect.ValueOf(object)
      // 因为我们传入的是指针对象,所以取出指针里面内容
      v = v.Elem()
      // 根据字段名获取字段的反射对象
      f := v.FieldByName( Age )
      // 判断字段是否能够设置
      if f.CanSet() {
         f.SetInt(20)
         println( Age设置成功 )
         return
      }
      println( Age设置失败 )
   }

   animal := Animal{
      Name:   Dog ,
      Age:   11,
      phone:  123456 ,
   }
   // 打印函数
   printValue(&animal)
   fmt.Printf( %+v , animal)
}

打印结果。从结果看,年龄成功设置为20

观察仔细的同学会发现,animal传入的时候,是传的指针,如果传的是对象本身,还能成功修改吗?对代码进行了简单的修改

func TestChangeObjectFail(t *testing.T)  {
   printValue := func (object interface{})  {
      // 获取反射对象
      v := reflect.ValueOf(object)
      // 注释掉是因为传入的对象不是指针
      //v = v.Elem()
      // 根据字段名获取字段的反射对象
      f := v.FieldByName( Age )
      // 判断字段是否能够设置
      if f.CanSet() {
         f.SetInt(20)
         println( Age设置成功 )
         return
      }
      println( Age设置失败 )
   }

   animal := Animal{
      Name:   Dog ,
      Age:   11,
      phone:  123456 ,
   }
   // 打印函数
   printValue(animal)
   fmt.Printf( %+v , animal)
}

打印结果。观察到Age设置失败。这是因为我们传的是Animal的值拷贝,反射的是拷贝对象,因此,不能通过反射对象,修改对象Animal的值。

Age设置失败
{Name:Dog Age:11 phone:123456}

三、参考文献

scholarworks.umass.edu/cgi/viewcon…

go.dev/blog/laws-o…

lucacardelli.name/Papers/OnUn…

www.hpi.uni-potsdam.de/hirschfeld/…