翻译了一篇 Go 官方博客介绍反射的文章:
- 原文地址
- 免版
简介
在计算机科学中,反射是一种在运行时检测自身结构(类型)的能力,反射构成元编程的基础,也是混乱的来源。
在这篇文章中我们会尝试澄清 Go 语言中的反射如何运作,每个语言的反射模型都不一样(典型如 Java),很多语言甚至不支持反射,因此在这篇文章中说明的只是 Go 语言反射。
类型和接口
因为整个反射模型构建在类型系统之上,我们先复习一遍 Go 中的类型。
Go 是静态类型语言,任何变量在编译时都有明确的类型,如 int、float32、*MyType, []byte
等类型...
type MyInt int
var i int
var j MyInt
复制代码
变量 i
的类型为 int
,变量 j
的类型为 MyInt
。它们两个明显有着不同的静态类型,除此之外又有着相同的基本类型 int
。因为静态类型不同,所以两者必须在转换后才能进行赋值。
接口类型是类型系统中非常重要的一个分类,其代表约定的方法集。接口变量可以存储任意的值,只要该值实现对应的接口方法集。io
包中的 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{}
:
interface{}
复制代码
其方法集为空表示任何类型都实现空接口,任何类型的值都可以对其赋值。
有些人说接口是动态类型,这种说法是不对的,它们是静态类型。一个接口类型变量总是拥有固定的静态类型,即使在运行时存储在接口中的值有不同的类型(类型实现接口的方法集)。
我们需要理解这些概念是因为反射和接口密切相关。
接口值
Russ Cox 写了一篇文章 Go Data Structures: Interfaces 详细解释了 Go 语言种的接口值。再次不必重复文章中的概念,下面对文章的简单总结:
接口类型变量存储一对值:
- value:赋值给接口类型变量的实际值;
- type:实际值的类型信息。
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
方法,接口中的类型值(the value inside)携带所有关于值的类型信息,因此我们可以实现下面的操作(类型信息都有自然可以断言):
var w io.Writer
w = r.(io.Writer)
复制代码
上面的赋值表达式称为类型断言,其断言 r
接口变量内部存储的 (value, type)
实现 io.Writer
接口,所以我们可以将其赋值给 w
。在赋值结束后,w
包含 (tty, *os.File)
,与我们之前在 r
中看到的一样。接口的静态类型决定了哪些方法可以通过该接口变量调用,即使内部存储的 (value, type)
拥有更大的方法集。
继续,我们还可以这样做:
var empty interface{}
empty = w
复制代码
我们的空接口 empty
依然会在内部存储相同的 (tty, *os.File)
。这意味着空接口可以存储任何值并拥有我们需要的所有信息。
在对空接口赋值时没有使用类型断言,因为任何值都满足空接口,w
显然实现空接口(方法集是空接口的超集)。而上面的 Reader
转换的 Writer
则不一样,我们需要显式使用类型断言是因为 Reader
接口不是 Writer
接口的超集。
一个重要的细节是接口内部总是存储 (value, concrete type),并不能存储 (value, interface type),接口内部并不存储接口值!
现在我们准备好研究反射了。
第一反射定律
反射从接口值中提取反射对象。
在最基本的概念上,反射只是一种检测存储在接口中的 type 和 value 的机制。因此我们需要理解 reflect 包中的两个类型 Type 和 Value。这两个类型提供访问接口变量内部存储的能力,并提供两个简单的函数 TypeOf
和 ValueOf
从接口变量中获取 Type
和 Value
(从 Value
得到 Type
也是一件很简单的事情,我们暂时保持两者在概念上的分离)
让我们从 TypeOf
开始:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
复制代码
输出:
type: float64
复制代码
你也许会想接口在哪里,看起来只传递 float64
类型的 x
变量作为参数给 TypeOf
函数,而不是接口变量。实际上 TypeOf
函数签名中的参数是空接口,x
会先赋值给空接口,然后作为函数参数传递,TypeOf
函数内部处理空接口恢复类型信息 Type
。
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
复制代码
ValueOf
函数也是通过类似的方法得到 Value
类型变量。
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
复制代码
输出:
value: <float64 Value>
复制代码
直接调用
String
方法是因为在默认情况下fmt
包直接深入Value
显示内部真正的值(3.4)。
Type
和 Value
都包含许多检测和操纵它们方法,一个重要的方法是 Value
的 Type
方法返回对应的 Type
类型值。另一个重要的方法是两者都拥有 Kind
方法返回常量基本类型(Uint、Float64、Slice
等)。通常 Value
上的 Int
、Float
等函数作用是提取内部存储的值。
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
作为参数和返回值。如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
方法返回静态类型对应的基本类型,例如下面x
的静态类型是MyInt
类型,基本类型是reflect.Int
。
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
复制代码
第二反射定律
反射从反射对象中提取接口值。
就像物理反射定律,与第一条定律相反,从反射对象逆向可以得到接口值。
通过 Value
的 Interface
方法可以恢复接口值,实际上这个方法打包 type
和 value
信息放到空接口中返回。
// 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
等函数做得更好,这些函数接收空接口值作为参数,并像上面学到的一样对这些参数进行解包。因此如果要直接打印 reflect.Value
的内容需要使用 Interface
函数获取接口值后传递。
fmt.Println(v.Interface())
复制代码
为什么不直接使用 fmt.Println(v)
?因为 v
是 reflect.Value
类型的值,我们想要的是实际存储的值。
fmt.Printf("value is %7.1e\n", v.Interface())
复制代码
输出
3.4e+00
复制代码
再次强调,这里不需要使用类型断言 v.Interface()
到 float64
是因为空接口内部存储的值和类型在 Printf
函数内部会被恢复。
简而言之,Interface
方法是 ValueOf
方法的逆方法,除了返回值总是静态类型 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 是 not addressable
的,而是说 v
是不可设置的,可设置(settability) 是 Value
的重要属性,并不是所有 Value
都是可设置的。
CanSet
方法检测值是否可设置。
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
复制代码
输出:
settability of v: false
复制代码
在不可设置的 Value
上调用 Set
方法会出错,那什么是可设置?
可设置(settability)有一点像地址可达(addressability),严格上说:**这是一个反射对象可以修改实际创建该反射对象的值的属性,可设置与否取决于反射对象是否持有原始值(指针)。
var x float64 = 3.4
v := reflect.ValueOf(x)
复制代码
当我们传递一个 x 的拷贝给 reflect.ValueOf
,所以参数的空接口值内部持有 x
的拷贝而不是 x
本身。
v.SetFloat(7.1)
复制代码
因此,如果这个语句执行成功,也不会更新 x
,即使 v
看起来是通过 x
创建的。反而会更新存储在 Value
中的复制值,真正的 x
并不受影响。上述情况容易产生混乱和困扰,因此在语言层面讲这种行为定义为非法的,通过判断可设置属性避免这个问题。
如果上面看起来有些奇怪,实际上并非如此,这只是熟悉情境的奇怪包装罢了(值传递和指针传递,拿到指针才可以修改原始的值)。
思考传递 x
给函数。
f(x)
复制代码
我们不会期望 f
能给修改 x
的值,因为我们传递给 f
的是 x
值的拷贝,而不是 x
本身。如果我们想要直接修改 x
,我们必须传递 x
的地址(指针)。
f(&x)
复制代码
上面的方式非常简单和直接,并且反射的工作原理也是一样的。如果我们想通过反射修改 x
,我们必须传递指针给 Value
。
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
指向的值。我们需要调用 Elem
方法,其间接通过指针取到原始值,并将结果存储到新的 Value
值中返回。
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
复制代码
现在 v
是可设置的反射对象。
settability of v: true
复制代码
自从 v
开始代表 x
,我们最终可以使用 v.SetFloat
方法修改 x
的值:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
复制代码
输出:
7.1
7.1
复制代码
尽管反射有些难以理解,但反射所做的一切都是语言层面支持的,也许 Value
和 Type
会掩饰所发生的事情。只要保持清醒,关注 Value
在被修改时需要指向某个地址。
Structs
在上面的例子中 v
只是指向一个基本类型,而更通用的问题是修改结构体的字段,当我们拥有结构体的指针,我们可以修改它的字段值。
下面是一个简单的例子用于分析一个结构体值。使用 T
类型的指针创建一个 Value
,因此在后续可以修改 t
。
声明并初始化 typeOfT
作为 t
的类型,并通过直接了当的方法 NumField
和 Field
提取出字段的名字、类型和值。
Value
的Field
还是Value
,并且是可设置的;Type
的Field
则是StructField
。
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
复制代码
还有一个关于可设置的知识点:只有以大写开头的字段才是可设置的(可导出字段)。
因为 s
包含可设置的反射对象(Elem 获得原始对象),我们可以修改结构体的字段。
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
复制代码
输出:
t is now {77 Sunset Strip}
复制代码
关于
- My Blog
- My Wechat: