golang的reflect使用

reflect 介绍
计算机科学中,反射(reflect) 指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行时能够“观察”并且修改自己的行为。
反射和内省(type introspection)不同,内省机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能运行在对程序自身信息进行,还要能进一步根据这些信息改变程序状态或结构。
早期语言汇编包含反射能力,动态修改指令或对它们进行分析等等反射功能时很平常的。编程发展到如C语言等高抽象层次语言时,这种实践消失了,带有反射特性的高级编程语言要更晚出现。
golang 作为一个诞生较晚的现代语言,自然也支持反射能力。通过标准库 reflect 包我们可以使用 golang 提供的这个能力。
Golang reflect 原则
interface 类型
reflect 建立在类型系统上,我们先从 Go 的类型系统开始讲起。Go 是静态类型语言,所有的变量都有对应的静态类型,因此所有变量的类型在编译时就已经确定:int,float32,*MyType,[]byte等等。
如果我们定义
| |
那么 i 是 int 类型,而 j 是 MyInt 类型。两个变量拥有不同的静态类型,尽管它们的底层类型是一样的,但是它们之间的赋值必须通过显式的类型转换完成。
interface type 是一类非常重要的类型,它代表一些方法的集合。interface 变量能够存储所有实现了 interface 中的方法的变量。一个广为人知的例子是 io.Reader 和 io.Writer。
| |
任何类型实现上述定义的 Read 或 Write 方法,我们认为该类型实现了 Reader 或 Writer 接口。
| |
很重要的一点是,无论 r 的值是什么,r 的类型永远是 io.Reader,go 是静态类型,而 r 的静态类型是 io.Reader 。
空接口类型是一个十分重要的类型。
| |
它代表空方法的集合,它可以接收任意值,包括零值或有方法的值。
有些人可能认为Go的interface是动态类型,其实这是误解,它们是静态类型:interface 类型的变量永远是相同的静态类型,但是存储在interface中的值在运行时可以修改类型,该值要求实现interface定义的方法集合。
interface 的细节
interface 类型的变量存储了一个对值:赋值给该interface的值;以及该值的类型。
| |
r 存储了(value, type)对:(tty, *os.File)。需要注意 *os.File 实现了比 Read 更多的方法,但是使用 Read interface只能访问Read 中的方法。
| |
通过赋值给 Write 接口,我们可以访问 tty 中实现的 Write 中的方法。
我们还可以这样
| |
空的interface仍然包含了相同的pair, (tty, *os.File)。它的用处是:空的interface可以接收任意值并保留该值的所有信息。
这里不需要进行类型断言,因为 Write 接口满足空interface类型要求,而 Read 接口和 Wirte 接口的方法集并不匹配,我们需要显式进行类型断言,告诉编译器我们知道它们存储的值是满足接口的。
还有很重要的一点要记住:interface中存储的pair是(value, concrete type),并不是 (value, interface type)。所以interface不能保存interface 值。
第一个法则: 可以从 interface 值获取反射对象
先说基本功能,反射是测试interface中存储的值和类型的机制。我们需要了解 package reflect 中的 Type 和 Value。这两个类型可以访问interface中的内容。reflect.TypeOf 和 reflect.ValueOf 用来从interface中获取 reflect.Type 和 reflect.Value 。
| |
反射对象的第二个属性是 Kind, 它是底层类型,非静态类型,例如反射对象包含用户自定义类型的值:
| |
v 的 Kind 是 reflect.Int,但是 x 的静态类型是 MyInt,不是int。
第二个法则: 可以从反射对象获取 interface 值
获得一个reflect.Value 后,我们可以使用 Interface() 方法恢复 interface 值。该方法将type和value打包放入interface表达式中,并将它返回。
| |
之后你可以这么处理
| |
我们还可以简化,fmt.Println 的参数是空 interface,他们在方法内部会做解包操作,因此我们只需要直接将interface传入。
| |
第三个法则:想要修改反射对象,对象的指必须是settable的
第三法则比较难懂,要结合法则一进行理解。
| |
结果会 panic 并输出信息。
| |
错误的原因是 v 不是 addressable 的,Settable 是 reflect.Value 的一个属性,不是所有的 reflect.Value 都有该属性。
CanSet 方法输出 settable 属性
| |
Settablity 是一个 bit 类似 addressability,但是更严格。这个属性表述反射对象可以修改创建该反射对象的原始变量的值。Settability 由反射对象是否持有原始对象决定。
当我们使用
| |
反射对象只持有了 x 的 copy,并不是原始对象,因此
| |
如果允许运行成功,那么它修改的也不是x,而是反射对象内部的一个值,这会让开发者产生疑惑。所以这并不被允许,settability 属性就是用来避免该问题。
如果我们想要修改原始对象,那么我们可以使用指针:
| |
这里 p 也是不可设置的,但是我们并不想修改 p,我们想要修改 p 指向的对象。我们可以使用 Elem 方法获取 p 指向的对象,该方法获取指针指向的元素,并保存成 reflect.Value 类型返回。
| |
在这里我们可以修改 x 了。
| |
结构体
在我们前面的例子中,v 并不是指针本身,而是它派生出来的对象。这种方法的一个使用场景是修改结构体的字段。只有我们拥有结构体的地址,我们才可以修改它的字段。
下面是一个分析结构体值的例子。我们使用结构体变量的地址构建一个反射对象,这样我们可以通过该对象对它进行修改。
| |
因为 s 是 settable 的,所以我们可以用它修改结构体字段。
| |
总结
再重复一遍三法则:
- 可以从interface值获取反射对象
- 可以从反射对象获取interface值
- 想要修改反射对象,对像必须是settable
一旦你理解这三个反射原则,那么Go就变得更容易使用了。反射是一个需要小心使用的强大工具,可以帮助你的代码减少不必要的重复。
下一次我将介绍 encoding/json 的实现,真正深入学习反射的使用。
参考
- https://www.wikiwand.com/zh-hans/%E5%8F%8D%E5%B0%84_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)
- https://golang.org/pkg/reflect/
- https://blog.golang.org/laws-of-reflection
- https://colobu.com/2018/02/27/go-addressable/
- https://studygolang.com/articles/938
- Go Data Structures: Interfaces
- https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/