在计算机科学中,反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力,属于元编程的范畴。在这篇文章中, 我们将尝试解释反射在Go中如何工作。
类型和接口
因为反射构建于类型系统之上,所以我们首先回顾一下Go的类型系统。
Go是一种静态类型语言。每个变量都有一个静态类型,也就是说,在编译时每个变量有且仅有一个已知的类型:int, float32, *MyType, []byte 等等。假设程序中保护如下代码:
type MyInt int
var i int
var j MyInt
那么变量 i
具有类型 int
,变量j
具有类型MyInt
。尽管变量i
和j
具有相同的基础类型,但它们仍然具有不同的静态类型。他们不能在没有类型转换的情况下彼此赋值。
有一类重要的类型就是接口类型,它们包含固定的方法集合。只要该值实现了所有的接口方法,接口变量就可以存储任何具体的(非接口)值。众所周知的一个例子就是io.Reader
和io.Writer
:
// 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
:Go是静态类型语言,r
的静态类型是io.Reader
接口类型中一个非常重要的例子是空接口:
interface{}
它表示空白方法集合,任何类型都可以赋予该类型的变量,因为任何类型都包含零个或多个方法。
某些人可能认为Go的接口是动态类型,但这是误导。它们是静态类型的:一个接口类型的变量总是具有相同的静态类型,即使在运行时存储在接口变量中的值可能会改变类型,该类型也将始终满足接口。
我们需要对这些内容进行准确的描述,因为反射和接口密切相关。
接口的实现方式
一个接口类型的变量存储了一对元素:分配给该变量的具体值,以及该值的类型描述符。更确切地说,该值是实现接口的具体数据项,该类型则描述了值的真实类型。比如:
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方法以外的其他方法,即使接口的值(r
)只提供了对Read方法的访问,但是值tty
包含了类型*os.File
的所有信息,这就是为什么我们可以折么做:
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的子集)
一个重要的细节是,接口内部的对总是具有(值,具体类型)这样的形式,而不是(值,接口类型)这样的形式,接口不能包含接口值。
现在,我们来讨论一下反射。
1. 从接口值到反射对象
从根本上来说,反射只是一种检查存储在接口变量内的类型和值对的机制。为了开始使用反射,我们需要了解reflect
包中的2类反射机制:Type和Value。这两类反射可以访问接口变量中的内容,而对应的两个简单的函数就是reflect.TypeOf和reflect.ValueOf,它们可以从接口变量中检索reflect.Type和reflect.Value。(另外,从reflect.Value中可以很容易得到reflect.Type,但现在让我们将Type和Value这两个概率分开)
让我们从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
。但实际上,reflect.TypeOf
函数的签名包含一个空接口:
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type
当我们调用reflect.TypeOf(x)
函数时,变量x
首先保存在一个作为参数传递的空接口中,reflect.TypeOf
解压这个空接口并还原类型信息。
同样的,reflect.ValueOf
函数将会还原对应的值信息:
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
程序将打印:
value: 3.4
类型reflect.Type
和reflect.Value
都包含了许多方法,让我们可以检查和操作它们。一个重要的例子是reflect.Value
包含一个Type
方法可以返回reflect.Value
对应的类型。另一个是reflect.Value
和reflect.Type
都具有的Kind
方法,它返回一个枚举,表示存储了什么类型的项:Uint, Float64, Slice 等等。除此以外,reflect.Value
中使用类型名字作为函数名的方法(Float, Int)可以让我们获取存储在里面的具体值:
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这样的方法,但是为了使用它们,我们需要了解可设置性,这是第三个反射定律的主题,后面会讨论。
反射库有一些值得特别指出的特性。首先,为了简化API,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
。换句话说,即使Type
方法可以区分int和MyInt,Kind
方法也不可以。
2. 从反射对象到接口值
就像物理反射一样,Go中的反射也可以产生它自己的反转。
给定一个reflect.Value
,我们可以使用Interface
方法恢复接口值。实际上该方法将类型和值信息从新打包回接口并返回结果:
// Interface returns v's current value as an interface{}.
func (v Value) Interface() (i 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)
?因为v
是一个reflect.Value
对象,我们希望获取它表示的实际值)。由于v
的实际值是float64
,我们也可以使用浮点数格式字符串进行打印:
fmt.Printf("value is %7.1e\n", v.Interface())
结果是:
3.4e+00
同样,不需要将v.Interface()
的结果断言为float64
,空接口参数中包含了具体的类型信息,Printf
会恢复它。
简而言之,Interface
方法与ValueOf
方法互反,只不过它的结果始终是静态类型interface {}
3. 为了修改反射对象,其值必须是可设置的
第三条法则最容易引起混乱,不过假如你是从第一条法则开始学习的,那也很容理解。
以下是一些无法正常工作的代码,但是值得研究:
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
是不可设置的。可设置性是reflect.Value
的一个属性,并不是所有的reflect.Value
都是可设置的。
CanSet
方法可以返回reflect.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
的副本,而不是x
本身。因此,如果语句:
v.SetFloat(7.1)
运行执行,它将不会更新x
,即使v
看起来像是通过x
创建的。相反,它会更新存储在反射对象中的值而x
本身不会受到影响。这会令人困惑,并且也是无意义的。因此这样的操作是非法的,而可设置性属性可用来避免此类问题。
这看起来很奇怪,其实并不是。想象一下把参数x
传递给一个函数:
f(x)
我们不希望函数f
可以修改参数x
,因为我们传递的是x
的副本,而不是x
本身。如果我们希望函数f
可以直接修改x
,我们必须将x
的地址作为参数传递给f
:
f(&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
指向的地方,我们使用一个称之为Elem
的方法,它通过指针进行间接寻址,并将结果保存在一个名为v
的reflect.Value
中:
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
反射可能很难理解,尽管reflect.Type
和reflect.Value
可以掩饰正在发生的事情,但它的确在做语言的工作。 请记住reflect.Value
需要某些对象的地址才能修改它们代表的内容。
结构体
在我们前面的例子中,v
本身不是一个指针,它只是从一个真正的指针派生而来。出现这种情况的一种常见方式是使用反射来修改结构体的字段。只要我们拥有结构体的地址,我们就可以修改它的字段。
这里有一个简单的例子,它分析一个结构体t
。我们使用结构体的地址创建反射对象,因为我们稍后需要修改它。然后,我们将typeOfT
设置为它的类型,并直接使用方法遍历它的字段。请注意,我们从结构体中提取字段名称,但是字段本身是常规的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
时将会失败,因为此时t
的字段将是不可设置的。
网友评论