Go的反射

介绍

反射能力是一个程序自己检查自己的结构的能力,是一种元编程(metaprogramming)的形式。

类型和接口(Types and interfaces)

我们必须明确以下三个概念,因为反射和它们息息相关。

区分三个词:

  • 静态类型(static type
  • 基础类型(underlying type
  • 接口类型(interface types

静态类型和基础类型

  • 可以说 Go 中的每一个类型都是静态类型。因为 Go 是静态类型的(Statically Typed)语言。即开发者自定义的类型也是一种静态类型。
  • 基础类型是指 GO 内部定义的几种类型,如 intfloat64 等等。

如下方示例,

  • MyInt 是一个静态类型,j 的类型(就是指静态类型)就是 MyInt
  • int 也是一个静态类型,同时它也是一个基础类型;
  • 所以 ij 有不同的类型(静态类型),但有相同的基础类型;
  • 静态类型不同的变量相互之间是不可以直接赋值的。
type MyInt int

var i int
var j MyInt

静态类型和动态类型的区别(参考)

如果在编译时就知道某一个变量是什么类型,这个语言就是静态类型的编程语言。
所以开发者在开发时必须为每一个变量指定类型,它的好处就在于编译器可以做一些检查工作,即在编译阶段就发现了一些 bug。

如果一种语言是动态的,那变量只在运行时有类型。
所以动态编程语言没有编译器检查它们在类型,这让开发变得快速。这类语言大多是脚本语言,即一般比较小,这样方便开发者自己去找 bug。

接口类型

接口类型是提供了一套固定的方法的类型。一个接口变量可以存储任何非接口的固定的值,只要这个值实现了接口的方法。

一个很好的例子就是 io.Readerio.Writer。下面这些使用都是可以的。

    var r io.Reader        // interface{}
    r = os.Stdin           // *io.File
    r = bufio.NewReader(r) // *bufio.Reader
    r = new(bytes.Buffer)  // bytes.Buffer
    // and so on

一定要注意: r 定义为 io.Reader 接口类型,不论 r 的值如何变化,是 os.Stdin*io.File 类型)还是 bufio.NerReader(r)*bufio.Reader 类型),它的静态类型都是 io.Reader

一个极重要的接口类型是空接口 interface{} 它表示了一组空的方法,因为任何类型都有零个或多个方法 ,所以值就可以是任何类型。(一些人说 Go 的 interface{} 是动态类型的,这是错误的。)

一个接口的表示

这里有一篇博文详细介绍了接口:Go Data Structures: Interfaces,可以看一下,这里简单总结一下。

一个接口类型的变量存了两部分:一个是具体的值,和这个值的类型描述符(type descriptor)。

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 实现了 io.Reader 的接口。
所以我们可以用如下方法:

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

这个表达示叫 类型断言(type assertion),上例中断言了 r 的内部也实现了 io.Writer,所以我们可以将它赋值给 w,所以 w 包含一对信息 (tty, *os.File)

进一步地,我们还可以这样做:

var empty interface{}
empty = w

这样我们的空接口就包含了相同的一对信息 (tty, *os.File)。这种方式很灵巧:一个空的接口可以包含任何值并且包含我们可能需要的所有信息。(这里不用断言,因为 it's known statically that w satisfies the empty interface)

有一点要注意,一个接口的内部的一对信息,必须是 (value, concrete type) 而不能是 (value, iinterface value)。接口不能有接口类型的值。

下面我们来介绍反射。

反射

反射有三条法则:

  • 从接口值反射为反射对象
  • 从反射对象反射为接口值
  • 更新值时必须是可更新的

从接口值反射为对象

reflect 包有两个类型: Type 和 Value,这两个类型让我们有方法 reflect.TypeOfreflect.ValueOf 获取到接口内部的信息 reflect.Typereflect.Value。(同样通过 reflect.Value 也可以得到 reflect.Type,我们稍后再说。)

interface{}
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))

当我们调用 reflect.TypeOf(x) 时,它会将 x copy 一份到空的 interface{} 类型中,然后在 TypeOf() 从空接口中恢复到类型信息。

reflect.ValueOf() 方法同样从接口中恢复到值信息。

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

(由于 fmt 自动会获取到具体的类型的值,所以这里我们用了 String() 方法。)

总览
  • reflect.Valuereflect.Type 都有 Kind() 方法是获取反射对象的 基础类型(underlying type),而不是静态类型。
  • reflect.Value 上有 Type() 方法,可以获取到值的类型
  • reflect.Value 上有 Int() 等方法可以获取对应的值
  • reflect.Value 上有 SetInt() 等方法,可以设置值(下面介绍)

从对象反射为接口值

给定一个 reflect.Value 我们可以恢复它对应接口的值,这用到 Interface() 方法。它会将 type 信息和 value 信息打包到 interface{} 中,并返回 interface{} 值。

// Interface returns v's value as an interface{}.
func (v Value) 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 是不可寻址的,可设置性也是反射的一个属性,可以通过 CanSet() 方法来获取。

为什么它是不可设置的呢?
因为上例中 reflect.ValueOf(x) 和其他普通方法 f(x) 一样,会 copy 一个 x 的值传入方法中,而不是 x 本身,所以如果我们需要通过反射修改一个变量的值时,必须传入它的地址 reflect.ValueOf(&x)

var x float64 = 3.4
p := reflect.ValueOf(&x)                     // Note: take the address of x.
fmt.Println("type of p:", p.Type())          // *float64
fmt.Println("settability of p:", p.CanSet()) // false

但这样仍不够,因为 p 的类型是一个指针,所以我们需要设置它对应的值,所以需要方法 Elem()。所以,变成下面:

var x float64 = 3.4
p := reflect.ValueOf(&x)                            // Note: take the address of x.
fmt.Println("settability of p:", p.Elem().CanSet()) // true
p.Elem().SetFloat(2.4)                              //
fmt.Println(x)                                      // 2.4

这样就完成了通过反射的方式,更新一个变量的值。重申:传入一定要是地址,并且修改的是地址中对应的值(Elem()方法获取值)。

结构体(Structs)

我们前面都是在以基础类型做示例,下面是一个结构体的例子。

t := T{23, "robin"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
fmt.Println(typeOfT) // main.T
for i := 0; i < s.NumField(); i++ {
    filed := s.Field(i)                // 返回类型 reflect.Value
    fmt.Println(typeOfT.Field(i).Name) // A B
    fmt.Println(filed.Type())          // int string
    fmt.Println(filed.Interface())     // 23 robin
    fmt.Println()
}
s.Field(0).SetInt(28)       // 设置值
s.Field(1).SetString("cai") // 设置值
fmt.Println(t) // {28 cai}

基本是一样的,不过结构体中

  • s := reflect.ValueOf(&t).Elem() 获取元素的值 Value
  • s.NumField() 获取值对应的长度
  • s.Field(i) 获取对应下标的元素的 Value

总结

记住这三条法则:

  • 反射可以从接口值反射为反射对象
  • 反射可以从反射对象反射为接口值
  • 要更新反射对象,其值必须可设置

反射中还有许多在这里没讲到,比如收发 channel、分配内存、用 slices 和 maps、调方法等。可以阅读一下其他文章。


附:

  • The Laws of Reflection
  • Go Data Structures: Interfaces
  • Effective Go

你可能感兴趣的:(Go的反射)