写在前面
本文翻译自Go官方博客,对翻译内容有疑问,请在评论指出。
介绍
计算机的反射是程序审查自身结构的能力,特别是通过类型来反射。反射也是元编程的一种形式。我们也总是被它搞得很迷惑。
这篇文章尝试通过介绍Go中的反射是如何工作的让大家理解反射。每个语言的反射模型是不一样的(甚至有很多语言是不支持反射的),这篇文章是关于Go的,所以下文中的反射特指 “Go中的反射”。
类型和接口
因为反射是建立在类型之上的,我们首先复习一下类型。
Go是静态类型语言。每一个变量都有一个静态类型,在编译时就可以清楚的知道每个变量的类型:int, float32, *MyType, []byte 等等。如果我们定义
type MyInt int
var i int
var j MyInt
i 的类型是int,j的类型是MyInt,变量i和j用明确的静态类型,尽管他们的底层真实类型是一样的(int),但是如果不通过类型转换,两个变量是不能相互指派的。
interface 类型是一个很重要的类型,它代表一组固定的方法。一个接口变量可以接受任何一个实现该接口方法的值。我们熟知的一对例子就是 io.Reader和io.Writer,Reader和Writer类型来自io package:
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
类型实现了Read (或 Write) 方法就可以说实现了io.Reader(或io.Writer)接口。我们讨论的目的是在说明:一个io.Reader类型的变量可以保存任意一个实现Read方法的值:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
必须清楚的知道,不管r存储的具体值是什么,r的类型永远是io.Reader: 是静态类型,r的静态类型是io.Reader。
一个非常重要的接口类型是空接口
interface{}
它相当于是一组空方法,能满足于任何值。因为任何值都是拥有>=0个方法的。
一些人会说Go的接口是动态类型,这是误导。一个接口类型变量总是同一个静态类型,即使在运行时,存储在接口变量中的值可能会更改类型,但该值将始终满足接口要求。
我们必须清楚的知道这些,因为反射和接口是密切相关的。
接口的表示
Russ Cox 已经写了一个关于Go接口值表示的博客detailed blog post ,没有必要再此重复说。但是下面有一个简单的总结
接口类型的变量存储一对信息:给变量分配的具体值Value和类型描述符Type,确切的说,Value是实现接口的底层具体数据项,Type描述数据项的类型。例如:
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
r 包含一对信息(Value,Type),(tty, *os.File)。值得注意的是*os.File类型实现了除Read之外的方法;尽管接口值只提供Read方法的访问权限。但值本身仍包含的所有其他类型。这就是为什么我们可以这样做:
var w io.Writer
w = r.(io.Writer)
上述表达式是类型断言;它断言r内的数据项也实现了io.Writer,所以我们可以分配给w。分配之后,w将包含信息对 (tty, *os.File)。与r持有的信息对是一样的。接口的静态类型表明什么方法可以被此接口变量调用,尽管真实的值可能包含很多其他方法。
接下来,我们可以这样做
var empty interface{}
empty = w
空接口值empty将会被分配包含同样的信息对(tty, *os.File)。一个空接口能被赋予任意的值并且包含我们需要的所有信息。
(这里我们并不需要类型断言,因为w满足空接口要求,上一个例子中,我们将一个值从 Reader转换成 Writer,我们需要一个明确的类型断言是因为Writer的方法并不是Reader方法的子集)
一个重要的细节是接口内的信息对始终具有(值,具体类型)的形式,而不是(值,接口类型)。 接口不保存接口值。
以上介绍完毕,我们现在来看反射
反射第一原则
反射从接口值变为反射对象
在基本层面上,反射是检查一个接口变量内部存储的类型和值的机制,开始之前,需要了解这两个类型 package reflect: Type and Value。这两个类型提供访问接口变量的访问入口:两个简单的函数reflect.TypeOf and reflect.ValueOf,它们能获取接口变量对应的 reflect.Type and reflect.Value 。(通过reflect.Value可以很容易获取到reflect.Type,但是现在暂时将Value和Type概念分开)
我们首先看TypeOf:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
程序打印
type: float64
您可能想知道接口在哪里,因为程序看起来像是在传递float64变量x,而不是接口值,对reflect.TypeOf来说。接口在这里:godoc reports,reflect.TypeOf的方法签名参数包括一个接口变量
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
我们调用reflect.TypeOf(x),x首先存储在一个空接口上,然后通过参数传递;reflect.TypeOf解析并恢复类型信息
reflect.ValueOf方法,会获取值(从这里开始,我们将只关注可执行代码)
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
打印:
value: <float64 Value>
(我们明确地调用String方式是因为fmt包默认会挖掘reflect.Value来显示内部具体值。 String方法不会。)
reflect.Type 和reflect.Value 都有很多方法让我们来操作。一个重要的例子是Value有一个Type方法返回一个reflect.Value的Type。还有一个是,Type and Value都有一个叫Kind的方法,改方法返回对应存储的一个常量标示:Uint,
Float64, Slice等等。Value里面还有一些像Int 和Float 的方法让我们可以获取存储在其中的值(如int64 and float64):
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
打印
type: float64
kind is float64: true
value: 3.4
还有一些方法像:SetInt 和SetFloat,但是要使用它们,我们需要了解一些规则,这是下面讨论的第三反射原则的主题。
反射库有几个值得特别指出的属性。首先,为了使应用编程接口简单,Value的“getter”和“setter”方法在最大的类型上操作,该类型可以保存所有有符号整数的值:例如,int64。也就是说,Value的Int方法返回int64,而SetInt 值则为int64;可能需要转换为涉及的实际类型:
var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.
第二个属性是反射对象的Kind,它描述的是基础类型,不是静态类型。 如果反射对象包含用户定义的整数类型的值,例如:
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
v对应的Kind仍然是reflect.Int,即使x对应的静态类型是MyInt,不是int。换句话说,Kind不能将int与MyInt区别开。虽然Type可以。
反射第二原则
反射从反射对象变为接口值
就像物理反射一样,Go的反射会生成自己的逆函数
利用reflect.Value的Interface方法可以恢复接口值,此方法将类型和值信息打包返回到接口值
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
因此我们可以这样:
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
打印反射对象v对应的float64的类型的值
不过,我们可以做得更好。 fmt.Println,fmt.Printf等参数都是空接口值传递,像前面示例中一样,由fmt包在内部对其进行解析。 因此,正确打印reflect.Value的内容所要做的就是将Interface方法的结果传递给fmt.Println(v interface{})
示例:
fmt.Println(v.Interface())
(为什么不使用fmt.Println(v)?因为v是reflect.Value;我们想要它拥有具体的值。)因为值是一个float64类型的,我们可以用浮点数格式打印结果:
fmt.Printf("value is %7.1e\n", v.Interface())
会得到如下结果
3.4e+00
再次声明,这里不需要通过类型断言将 v.Interface()的结果转变成float64类型;空接口值内部含有具体值的类型信息,Printf可以恢复类型信息。
简而言之,Interface()是ValueOf()的逆方法,除非Interface()的结果总是静态类型interface{}
重申: 接口值<===>反射对象是一个可逆过程
反射第三原则
要修改反射对象,该值必须是可设置的
第三个原则最微妙且让人迷惑,但是我们如果是从第一个原则开始的是很容易理解的
这里有些代码不能正常运行,但是值得我们学习
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
如果你运行这段代码,你会得到一个难以理解的panic信息
panic: reflect.Value.SetFloat using unaddressable value
此问题并不是说值7.1是不可寻址的,而是说v是不可设置的。可设置性是一个反射Value的属性,但并不是所有的反射Values都是可设置的。
Value对应的CanSet方法会指出Value是否可设置
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
打印结果
settability of v: false
调用一个不可设置的Value值对应的Set方法会得到一个错误,那么什么是可设置性呢?
可设置性是有点像可寻址行,但是更严格。它是反射对象可以修改实际存储值的属性。可设置性取决于反射对象是否拥有原始的数据项,当我们说
var x float64 = 3.4
v := reflect.ValueOf(x)
我们传递x的值拷贝给reflect.ValueOf,并非x本身
v.SetFloat(7.1)
因此如果上述语句被允许成功执行,x的值并不会更新。更新的值是x的拷贝数据,这个操作是没有意义的而且会给人带来困惑。所以此操作是非法的,可设置性是用于避免此问题的属性
考虑将x传递给一个方法:
f(x)
我们不会指望f会去更新x因为我们传递的是x的值拷贝,并不是x本身。如果我们想要更新的话,我们可以将x的地址传递过去(x的指针)
f(&x)
这看起很直接且很熟悉,反射也是同样的工作方式。如果我们想要通过反射更新x,我们必须给反射库x对应的指针
我们这么做,首先初始化x,然后创建x指针对应的反射对象p
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
输出如下:
type of p: *float64
settability of p: false
p是不可设置的,我们并不是要去设置p,而是*p(具体值)。我们调用Value的Elem方法获取p对应的具体值。Elem方法通过指针返回一个反射Value v
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
如结果输出所示,现在v是一个可设置的反射对象,
settability of v: true
并且因为它代表了x,我们是可以通过v.SetFloat来修改x的值:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
像预期返回的一样:
7.1
7.1
反射可能很难理解,但是它确实在做语言所做的事情,尽管通过反射Types和Values可以掩盖发生的事情。但是 请记住,反射值需要内容的地址才能修改其表示的内容。
结构体
在我们前面的例子中,v本身不是指针,它只是从一个指针派生出来的。出现这种情况的常见方式是使用反射来修改结构的字段。只要我们有结构的地址,我们就可以修改它的字段。
这里有一个简单的例子来分析结构值t。我们用结构的地址创建反射对象,因为我们希望以后修改它。然后,我们将typeOfT设置为它的类型,并使用简单的方法调用遍历字段(请参见package reflect )。请注意,我们从结构类型中提取字段的名称,但是字段本身是常规的reflect.Value对象。
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
程序输出如下:
0: A int = 23
1: B string = skidoo
还涉及可设置性的一点是T的字段名称都是大写的(已导出),因为仅可导出的字段才是可设置的。
因为s包含可设置的反射对象,所以我们可以修改此结构体的字段
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
结果:
t is now {77 Sunset Strip}
如果我们修改程序,将s的来源从&t修改成t,再调用SetInt和SetString方法将会得到一个失败的结果
总结
反射原则如下:
- 反射从接口值变为反射对象
- 反射从反射对象变为接口值
- 要修改反射对象,该值必须是可设置的
一旦你理解这些原则,反射将很容易使用。尽管它还是很微妙。反射是一个强大的工具,除非绝对必要,否则应该小心使用并避免使用。
还有很多关于反射的知识在这里没有涉及到——在channels中接受/发送、内存分配、使用slices和maps、调用函数和方法——但是这篇文章内容已经足够多了,我们将在后面的文章中讨论其他的主题