Go 之反射基本思想

介绍

计算反射是程序检查其自身结构的能力,尤其是通过类型。这是元编程的一种形式。这也是造成混乱的重要原因。

在本文中,我们尝试通过解释反射在Go中的工作原理来澄清一些事。 每种语言的反射模型是不同的(许多语言根本不支持它),但是本文是关于Go的,因此对于本文的其余部分,应将“反射”一词理解为“ Go中的反射”。

类型和接口

因为反射建立在类型系统上,所以让我们从Go语言中的类型开始。

Go是静态类型的。每个变量都有一个静态类型,也就是在编译时已知并固定的一种类型:int,float32,* MyType,[] byte等。如果我们声明

type MyInt int

var i int
var j MyInt

那么我的类型为int,j的类型为MyInt。变量i和j具有不同的静态类型,尽管它们具有相同的基础类型,但是如果不进行转换就无法将它们彼此赋值。

类型的一个重要类别是接口类型,它表示固定的方法集。 接口变量可以存储任何具体的(非接口)值,只要该值实现接口的方法即可。 一对著名的示例是io.Reader和io.Writer,它们是io包中的Reader和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的接口是动态类型的,但这会产生误导。 它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也将始终满足接口的要求。

我们需要对所有这些事情都保持清醒的认识,因为反射和接口密切相关。

接口的表示

Russ Cox写了一篇详细的博客文章,内容涉及Go中接口值的表示。这里没有必要重复完整的故事,但是为了使摘要更简洁。

接口类型的变量存储一对:分配给该变量的具体值以及该值的类型描述符。 更确切地说,该值是实现接口的基础具体数据项,而类型则描述该项的完整类型。 例如,之后:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

r示意性地包含(值,类型)对(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的子集。)

一个重要的细节是,接口内的对始终具有形式(值,具体类型),而不能具有形式(值,接口类型)。接口不保存接口值。 现在我们准备好反射了。

反射的第一条定律

反射从接口值到反射对象。

在基本级别上,反射只是一种检查存储在接口变量中的类型和值对的机制。首先,我们需要在反射包中了解两种类型:TypeValue。这两种类型允许访问接口变量的内容,还有两个简单的函数,称为reflect.TypeOfreflect.ValueOf,从接口值中检索reflect.Typereflect.Value。(此外,从reflect.Value可以很容易地到达reflect.Type,但是现在让Value和Type概念保持分离。)

让我们从TypeOf开始:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var f float64
    fmt.Println(reflect.TypeOf(f))
}

查看打印结果:

float64

您可能想知道接口在哪里,因为该程序看起来像是在向reflect.TypeOf传递float64变量x而不是接口值来反映。但是它在那里;当godoc报告时,reflect.TypeOf的签名包括一个空接口:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用reflect.TypeOf(x)时,x首先存储在一个空接口中,然后将其作为参数传递; Reflection.TypeOf解压缩该空接口以恢复类型信息。

当然,reflect.ValueOf函数可以恢复值(从这里开始,我们将省略样板并只关注可执行代码):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

打印结果:

<float64 Value>

(我们明确地调用String方法,因为默认情况下,fmt包会挖掘到一个reflect.Value以显示其中的具体值。String方法不会。)

reflect.Typereflect.Value都有很多方法可以让我们检查和操作它们。一个重要的示例是Value具有Type方法,该方法返回reflect.ValueType。另一个是TypeValue都有Kind方法,该方法返回一个常量,指示存储的项目类型:Uint,Float64,Slice,等等。同样,使用诸如Int和Float之类的Value方法可以让我们获取存储在其中的值(如int64和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

还有诸如SetIntSetFloat之类的方法,但是要使用它们,我们需要了解可沉降性,这是第三反射定律的主题,下面将进行讨论。

反射库具有几个值得一提的属性。 首先,为使API保持简单,Value的“getter”和“setter”方法在可以容纳该值的最大类型上运行:例如,所有有符号整数的int64。 也就是说,ValueInt方法返回一个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.

查看结果:

type: uint8
kind is uint8:  true

第二个属性是反射对象的Kind方法描述基础类型,而不是静态类型。如果反射对象包含用户定义的整数类型的值,例如:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int
    var t MyInt = 10
    v := reflect.ValueOf(t)
    fmt.Println(v.Type())
    fmt.Println(v.Kind())
}

查看控制台结果:

main.MyInt
int

即使t的静态类型是MyInt而不是int,v的种类仍会反映int。换句话说,即使Type可以,Kind也不能将My和Int区别开。

反射第二定律

反射从反射对象到接口值

像物理反射一样,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)?因为v是reflect.Value;我们想要它包含的具体值。)由于我们的值是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: reflect.Value.SetFloat using unaddressable value

问题不在于值7.1是不可寻址的。这是v不可设置的。可设置性是反射值(Value)的属性,并非所有反射值都具有它。

值的CanSet方法报告值的可设置性;就我们而言:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

打印结果:

settability of v: false

在不可设置的值上调用Set方法是错误的。但是什么是可设置性?

可设置性有点像可寻址性,但是更严格。 它是反射对象可以修改用于创建反射对象的实际存储的属性。 可设置性由反射对象是否保留原始项目确定。 当我们说:

可设置性有点像可寻址性,但是更严格。 它是反射对象可以修改用于创建反射对象的实际存储的属性。 可设置性由反射对象是否保留原始项目确定。 当我们说

var x float64 = 3.4
v := reflect.ValueOf(x)

我们将x的副本传递给reflect.ValueOf,因此,作为reflect.ValueOf的参数创建的接口值是x的副本,而不是x本身。因此,如果声明

v.SetFloat(7.1)

被允许成功,即使v看起来是从x创建的,它也不会更新x。相反,它将更新存储在反射值内的x的副本,并且x本身将不受影响。那将是混乱和无用的,因此是非法的,可设置性是避免此问题的属性。

如果这看起来很奇怪,那不是。实际上,这是在不寻常的服装中熟悉的情况。考虑将x传递给函数:

f(x)

我们不希望f能够修改x,因为我们传递了x值的副本,而不是x本身。如果我们想让f直接修改x,则必须将x的地址(即指向x的指针)传递给函数: f(&x)

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,实际上是*p。为了得到p所指向的内容,我们称为Value的Elem方法,该方法通过指针进行间接操作,并将结果保存在名为v的反射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

反射可能很难理解,但它确实在做语言的工作,尽管通过反射TypesValues可以掩盖正在发生的事情。请记住,反射值需要某些内容的地址才能修改其表示的内容

struct

在我们前面的示例中,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}

如果我们修改程序,以便从t而不是&t创建s,则对SetInt和SetString的调用将失败,因为t的字段不可设置。

结论

这又是反射定律:

  1. 反射从接口值到反射对象。
  2. 反射从反射对象到接口值。
  3. 要修改反射对象,该值必须可设置。

一旦理解了这些定律,尽管Go中的反射仍然很微妙,但它变得更易于使用。这是一个功能强大的工具,应谨慎使用,除非绝对必要,否则应避免使用。

我们还没有涉及到很多发射,包括在通道上发送和接收,分配内存,使用切片和映射,调用方法和函数,但是这篇文章足够长。 我们将在以后的文章中介绍其中一些主题。

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信:okayGoHome
本帖由系统于 3年前 自动加精
Dennis_Ritchie
讨论数量: 2

var x float = 3.4 fmt.Println("value:",reflect.ValueOf(x).String()) 输出不是 value:float64 吗

3年前 评论

go 的反射 可以向PHP java 一样使用 写 ioc 吗

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!