什么是反射

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

为什么使用反射

反射给人的第一印象是:使用起来很危险,能不用就不要用,但是go语言既然提供了这个能力,必然有其不可替代的作用:

有时我们需要一个函数,有能力统一处理各种类型,比如fmt.Printf

reflect包

reflect包的两个重要的类型

  • reflect.Type: 是一个有很多方法的接口,这些方法可以识别类型,以及透视类型的组成部分。同时满足fmt.Stringer(实现了String方法,可以通过fmt.Printf 打印)

    • reflect.TypeOf():接收一个任意类型的参数,返回reflect.Type。因为返回的是一个接口值对应的动态类型,所以返回的总是具体类型:

      var w io.Writer = os.Stdout    // 接口类型是io.Writer
      fmt.Println(reflect.TypeOf(w)) // 反射后返回的是具体的实现类型:*os.File
  • reflect.Value: 是一个有很多方法的结构体,可以包含任意类型的值。也满足fmt.Stringer,但除非包含的是一个字符串,否则String方法的结果仅仅暴露了类型

    • reflect.ValueOf(): 接收任意的interface{},并将接口的动态值以reflect.Value形式返回。
    • 和接口interface{}的区别:空接口隐藏了布局信息、内置操作和相关方法,除非使用类型断言,否则我们对所包含的值能做的事很少。因为go语言本身允许自定义类型,无法使用类型断言判断所有类型。但是可以使用reflect.Value的Kind方法来区分不同的类型,尽管有无限种类型,但类型的分类(Kind)只有少数几种

如何使用

可以查看go圣经的 display方法范例

需要关注的是:

  • 尽管reflect.Value有很多方法,但对于不同的Kind,只有少量的方法可以安全调用
  • 非导出字段在反射下也是可见的
  • 所有的Kind 参见官方文档

使用reflect.Value修改值

在通过反射修改值前,需要了解什么是可寻址,什么是不可寻址。懒得总结了,参考网上的一篇文档

事实上,通过reflet.ValueOf(x)返回的reflect.Value都是不可寻址的。判读一个值是否可寻址,使用CanAddr方法

func main() {
	x := 2
	a := reflect.ValueOf(2)
	b := reflect.ValueOf(x)
	c := reflect.ValueOf(&x)
	d := c.Elem()        // 调用reflect.ValueOf(&x).Elem()来获得任意变量x可寻址的value值
	fmt.Println(a.CanAddr())  // false
	fmt.Println(b.CanAddr())  // false
	fmt.Println(c.CanAddr())  // false
	fmt.Println(d.CanAddr())  // true
}

通过可寻址的reflect.Value获取变量的指针,进而通过指针更新变量的值

  1. 调用Addr(),返回一个Value,其中包含一个指向变量的指针
  2. 在这个value上调用interface(),会返回一个包含这个指针的interface{}值
  3. 判断变量的类型,通过类型断言把接口内容转换成一个普通指针
func main() {
	x := 2
	d := reflect.ValueOf(&x).Elem()
	px := d.Addr().Interface().(*int)
	*px = 3
	fmt.Println(x) // 3
}

在一个可寻址的reflect.Value上,通过reflect.Value.Set更新变量

同样需要注意类型的匹配,否则程序会崩溃

func main() {
	x := 2
	a := reflect.ValueOf(&x).Elem()
	a.Set(reflect.ValueOf(4))
	// a.SetInt(4) go为一些基本类型特化的set变种,SetInt,SetUint,SetString等
	fmt.Println(x)
}

如果一个可寻址的reflect.Value包含一个未导出的字段,则是不允许修改的

通过CanSet方法能够判断一个reflect.Value是否可寻址并且可修改

不能滥用反射

  1. 反射的代码很脆弱:go是一种编译型语言,在编译过程中就能够检测类型错误。但是反射代码只能在运行时才能检测出来;

  2. 大量使用反射代码难以理解:因为类型也算是某种形式的文档,而反射相关的操作无法做静态类型检查;

  3. 反射函数比特定类型优化的函数慢一两个数量级:对于关键路径上的函数避免使用反射;

使用场景

  • http bind参数
  • fmt, encoding/json, encoding/xml, text/template, html/template