Golang反射
什么是反射
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
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获取变量的指针,进而通过指针更新变量的值
- 调用Addr(),返回一个Value,其中包含一个指向变量的指针
- 在这个value上调用interface(),会返回一个包含这个指针的interface{}值
- 判断变量的类型,通过类型断言把接口内容转换成一个普通指针
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是否可寻址并且可修改
不能滥用反射
反射的代码很脆弱:go是一种编译型语言,在编译过程中就能够检测类型错误。但是反射代码只能在运行时才能检测出来;
大量使用反射代码难以理解:因为类型也算是某种形式的文档,而反射相关的操作无法做静态类型检查;
反射函数比特定类型优化的函数慢一两个数量级:对于关键路径上的函数避免使用反射;
使用场景
- http bind参数
- fmt, encoding/json, encoding/xml, text/template, html/template