Go中的反射模型

    科技2022-07-11  82

    文章目录

    类型和接口接口的表示形式从接口反射出对象从反射对象到接口值修改反射对象的值对结构体类型的值进行发射总结 反射是指程序检查自身结构的能力,尤其是通过类型;它是一种元编程,也是产生混乱的重要根源。

    类型和接口

    反射建立在类型系统之上,所以让我们从go语言的类型开始。

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

    type MyInt int var i int var j MyInt

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

    接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。一个典型的例子就是 io 包 的 io.Reader 和 io.Writer,Reader 和 Writer类型:

    // Reader 是封装基本 Read 方法的接口 type Reader interface { Read(p []byte) (n int, err error) } // Writer 是封装基本 Write 方法的接口 type Writer interface { Write(p []byte) (n int, err error) }

    任何使用此签名实现 Read (或 Write)方法的类型都被称为实现了 ``io.Reader (或 io.Writer)接口。这意味着 io.Reader 类型的变量可以保存实现了 Read `方法的任何值:

    r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) // 等等

    需要明确的是,不管 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

    什么是类型的具体描述?类型的描述分为两部分,第一部分是类型的具体名称,第二部分是该类型的方法,或者说方法集。类型的方法可以是0个也可以是多个,但是无论如何,这些类型都是满足空接口类型的,因为空接口类型不需要你去实现任何方法集。

    r 中包含(value, type) 对,即(tty, *os.File)。请注意,类型 *os.File实现的方法不只有 Read;尽管接口仅提供对 Read 方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:

    var w io.Writer w = r.(io.Writer)

    该表达式中的赋值时类型断言。它断言的是 r 中的数据项也实现 io.Write,因此我们可以将其分配给 w。赋值后,w 中会包含该 (value, type) 对,(tty, *os.File),这与 r 中所持有的相同。接口的静态类型决定了接口变量可以调用哪些方法,尽管其内部的具体类型可能有更大的方法集。

    我们还可以将一个接口类型变量赋值给空接口类型的变量:

    var empty interface{} empty = w

    此时当我们调用empty变量时,会发些我们无法访问任何一个方法,因此此时的方法集为空。

    当我们讨论接口的时候,还必须讨论另一个概念:什么是多态?

    多态从字面上来说就是一个多种形态,它其实指的是一个对象可以进行多种形态的切换。在面向对象的编程范式中,我们可以定义我们自己的类,同时也可以定义我们的父类。通常父类包含了各个子类的一些通用方法和属性。而子类其实是特殊的父类。当我们将对象转换为父类对象时,此时我们所拥有的属性和方法都是父类对象的属性和方法。因此我们无法调用子类对象一些特殊方法。

    比如动物是猫和狗的统称,只要是动物都会有一些通用的行为和属性,比如睡觉,觅食。但是猫和狗又有自身特殊的行为和属性,比如猫抓老鼠,狗看门,而猫不会看门,狗也不抓老鼠,这是狗和猫各自拥有的特殊行为。这些行为没有被抽象到父类中,因此是它们独占的。当我们将对象切换为猫类时,我们就拥有了猫的属性和行为,而当我们切换为父类对象时,我们此时是动物,因此不具有猫的特殊行为抓老鼠。上面举的例子就是多态的一种。

    从接口反射出对象

    从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解 反射包 的两个类型:Type 和 Value,通过这两个类型可以访问接口变量的内容。还有两个函数 reflect.TypeOf 和 reflect.ValueOf,它们可以从接口值中取出 reflect.Type 和 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

    此时你可能会困惑,我们的反射机制是将存储在接口变量中的类型和值对反射出来,但是这里明明是一个具体的类型,并非接口类型,为啥我们依然可以对它进行操作?让我们看一下TypeOf的函数签名

    // TypeOf 返回 interface{} 中值的反射类型 func TypeOf(i interface{}) Type

    通过签名可知,该函数的形参为空接口类型的变量,此时我们定义的x变量其实是被保存在了该函数的形参i中,而该形参的类型是空接口类型。因此我们实际上是对空接口类型进行反射,而不是具体类型。

    reflect.TypeOf 从该空接口中恢复类型信息。相应的,reflect.ValueOf 函数会恢复值信息:

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

    打印:

    value: <float64 Value>

    我们调用String 方法,因为默认情况下,fmt 包会深入底层获取一个reflect.Value 来显示其中的具体值。String 方法不会这样。)

    func Println(a ...interface{}) (n int, err error)

    其实Println的形参也是空接口类型,因此我们传入的参数实际都被存储在了空接口类型的变量中,但是正如上面所述,fmt包会深入底层获取一个reflect.Value的具体值,因此我们每次打印接口类型变量都能把具体的值给打印出来。

    reflect.Type 和 reflect.Value 都有许多方法可以让我们执行检查和操作。一个重要的例子是 Value 具有 Type 方法, 该方法返回reflect.Value 的 Type 类型。另一个例子是 Type 和Value 都有一个 Kind方法,该方法返回一个标识存储的数据项类型的常数:Uint, Float64, Slice,等等。还有 Value 的很多方法,名字类似于Int 和Float,可以让我们获取存储在里面的值(如 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

    还有诸如SetInt 和 SetFloat 之类的方法,但是要使用它们,我们需要理解 settability,这是反射第三定律的主题,后续再进行讨论。

    反射库具有几个值得一提的属性。首先,为了保持 API 的简单,Value 的 “getter” and “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 返回一个 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 方法恢复接口值;实际上,该方法将类型和值信息打包回接口表示形式并返回结果:

    //接口返回v的值作为接口{}。 func (v Value) Interface() interface{}

    结果,我们可以说

    y := v.Interface().(float64) // y的类型为float64 fmt.Println(y)

    打印反射对象 v 表示的 float64 值。

    不过,我们可以做得更好。 fmt.Println,fmt.Printf 等的参数都作为空接口值传递,然后由 fmt 包内部解压缩我们在前面的示例中一直在做。因此,正确打印 reflect.Value 内容的全部工作就是将 Interface 方法的结果传递给格式化的打印例程:

    fmt.Println(v.Interface())

    简而言之,Interface 方法是 ValueOf 函数的反函数,除了它的结果始终是静态类型的 interface {}。

    重申:反射从接口值到反射对象,然后再返回。

    修改反射对象的值

    var x float64 = 3.4 v:= reflect.ValueOf(x) v.SetFloat(7.1)//错误:会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())

    在不可设置的 Value 上调用 Set 方法是错误的。

    Settability 有点像可寻址性,但是更严格。它是反射对象可以修改用于创建反射对象的实际存储的属性。Settability 由反射对象是否持有原始的数据项决定。当我们这样做时:

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

    我们实际上是把 x 的副本传递给了 reflect.ValueOf,因此作为 reflect.ValueOf 参数的接口值是 x 的副本,而不是 x 本身。因此,如果下面的操作:

    v.SetFloat(7.1)

    被允许执行,它也不会更新 x 的值,尽管 v 看起来像是从 x 创建的。相反,它会更新存在反射值内的 x 的副本, x 却不会受到影响。那将引起混乱并且是无用的,因此这种操作被定为非法的,settability 正是用来避免这种问题的属性。

    这实际上是一种很常见的操作。考虑将 x 传递给函数:

    f(x)

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

    f(&x)

    这种操作很常见,而反射也是以相同的方式工作的。如果我们想要通过反射修改 x 的值,则必须为反射库提供一个指向要修改的值的指针。

    让我们开始吧。首先,我们像往常一样初始化 x,然后创建一个指向它的反射值,称为 p。

    var x float64 = 3.4 p := reflect.ValueOf(&x) // 注意:取 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 := 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 设置为其类型,并使用简单的方法调用对字段进行迭代。请注意,我们从结构类型中提取了字段的名称,但是字段本身是常规的 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.", 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 < aaaa>将失败,因为无法设置 t ` 的字段。

    总结

    反射的三条定律:

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

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

    要修改反射对象,该值必须可设置。

    Processed: 0.015, SQL: 8