0%

golang 反射

类型和接口

Go 是静态类型语言。每一个变量都有一个静态的类型,即在编译时类型已知且固定:比如 intfloat32

接口类型

接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。如:

1
2
3
4
5
6
7
8
9
// 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(p []byte) (n int, err error) 方法的类型都被称为实现了 Reader 接口(Writer 同理)。这意味着 Reader 可以保存实现了 Read 方法的任何值:

1
2
3
4
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)

需要明确的是,不管 r 可能包含什么具体值,r 的类型始终是 io.Reader:Go 是静态类型的语言,而 r 的静态类型是 io.Reader

空接口

接口类型的一个非常重要的示例是空接口:

1
interface{}

它表示空的方法集,并且任何值都满足空接口,因为任何值都有零个或者多个方法。

有人说 Go 的接口是动态类型的,但这会产生误导。接口是静态类型的:接口类型的变量始终具有相同的类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。

接口的表示形式

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

1
2
3
4
5
6
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 方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:

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

因为 r 的具体类型里面包含了 Write 方法,而 r 里面包含的值依然持有它原来的值,所以这个断言是没有问题的。

一个重要的细节是,接口内始终保存 (值, 具体类型) 形式的元素对,而不会有 (值, 接口类型) 的形式。接口内部不持有接口值。

反射

反射第一定律:从接口值反射出反射对象

反射对象主要有两类:reflect.Typereflect.Value

从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解反射包的两个类型:TypeValue, 通过这两个类型可以访问接口变量的内容。还有两个函数 reflect.TypeOfreflect.ValueOf,它们可以从接口值中取出 reflect.Typereflect.Value。(另外,从 reflect.Value 可以很容易地获取到 reflect.Type,但是让我们暂时将 ValueType 的概念分开。)

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
// 打印 type: float64
fmt.Println("type:", reflect.TypeOf(x))
}

上面的代码看起来像将 float64 类型的变量 x 传递给了 reflect.TypeOf,而不是传递的接口值。但实际上,传递的是接口;

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

当我们调用 reflect.TypeOf(x) 时,x 先被存在一个空接口中,然后再作为参数传递;reflect.TypeOf 从该空接口中恢复类型信息。

相应的,reflect.ValueOf 函数会恢复值信息。

1
2
3
var x float64 = 3.4
// value: <float64 Value>
fmt.Println("value:", reflect.ValueOf(x).String())

reflect.Typereflect.Value 都有许多方法可以让我们执行检查和操作: * Value 具有 Type 方法,该方法返回 reflect.ValueType 类型。 * TypeValue 都有一个 Kind 方法,该方法返回 go 的类型(语言本身的类型,而不是自定义的类型) * Value 的很多方法,名字类似于 IntFloat64,可以让我们获取存储在里面的值。 * 还有诸如 SetIntSetFloat 之类的方法,可以修改接口的值。

反射第二定律:从反射对象到接口值

给定 reflect.Value,我们可以使用 Interface() 方法恢复接口值;

实际上,该方法将类型和值信息打包回接口表示形式并返回结果:

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

结果,我们可以说

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

打印反射对象 v 表示的 float64 值。一种更简洁的写法是:

1
2
// fmt.Println 本身就接受 interface{} 参数
fmt.Println(y)

反射第三定律:要修改反射对象,该值必须可设置

不可设置的例子:

1
2
3
4
var x float64 = 3.4
v:= reflect.ValueOf(x)
// panic: reflect.Value.SetFloat using unaddressable value
v.SetFloat(7.1)//错误:会panic错误。

因为调用 reflect.ValueOf(x) 的时候,函数只拿到了 x 的副本,而不是 x 变量本身,如果我们在函数内部修改了 x 那也只是修改了副本而已。

ValueCanSet 方法报告 Value 的可设置性:

1
2
3
4
var x float64 = 3.4
v:= reflect.ValueOf(x)
// false
fmt.Println("settability of v:", v.CanSet())

如果我们想修改它,可以在反射的时候,直接使用 x 的指针:

1
2
3
4
5
6
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:取 x 的地址
// type of p: *float64
// settability of p: false
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

我们注意到,这里我们使用了指针,但依然是不能设置其值。这是因为反射对象 p 是不可设置的,实际上我们想要设置的不是 p,而是 *p。为了获取 p 指向的内容,我们调用 Value 值的 Elem 方法,该方法指向指针:

1
2
3
v := p.Elem()
// settability of v: true
fmt.Println("settability of v:", v.CanSet())

现在,v 是一个可设置的反射对象了,我们可以使用 v.SetFloat 来修改 x 的值了:

1
2
3
4
5
v.SetFloat(7.1)
// 7.1
fmt.Println(v.Interface())
// 7.1
fmt.Println(x)

反射值需要变量的地址才能修改其表示的值。

结构体

在下面的例子中,我们使用结构体的地址创建反射对象,因为稍后将要对其进行修改。然后我们将 typeOfT 设置为其反射类型, 并使用简单的方法调用对字段进行迭代。

1
2
3
4
5
6
7
8
9
10
11
12
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())
}
1
2
0: A int = 23
1: B string = skidoo

此处传递的内容还涉及可设置性的另一点:T 的字段名是大写(已导出),因为只能设置结构体的导出字段。

因为 s 包含可设置的反射对象,所以我们可以修改结构的字段:

1
2
3
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

如果我们修改代码从 t 而不是 &t 创建 s,则对 SeteIntSetString 的调用将失败,因为无法设置 t 的字段。

结论

反射定律:

  • 反射可以从接口值到反射对象
  • 反射可以从反射对象到接口值
  • 要修改反射对象,该值必须可设置。

参考文档