Go 语言基础:反射¶
理解反射¶
如果程序能照镜子,会发生什么?
给程序一面镜子¶
在计算机科学中,反射(英语:reflection)是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射给程序提供了一面镜子,让程序能在运行时审视自身的结构,了解自己的类定义、方法签名、字段属性,甚至可以在运行时动态调用方法、修改属性值。这种观察自身并动态改变自身行为的能力使得程序具备了更高的灵活性。
为了建立初步印象,我们先通过两个简单的例子看看 Java 和 Python 给程序提供的镜子是什么样的。在 Java 中,我们可以通过 Class 类获取任意对象的类型信息:
// 获取对象的Class对象
Class<?> clazz = obj.getClass();
// 获取所有公共方法
Method[] methods = clazz.getMethods();
// 动态调用方法
Method method = clazz.getMethod("methodName", parameterTypes);
method.invoke(obj, args);
而在 Python 中,反射能力更为直接和强大,使用内置函数可以轻松获取对象的属性和方法,动态地修改属性值或调用方法:
# 获取对象的所有属性和方法
attrs = dir(obj)
# 检查对象是否有某个属性
if hasattr(obj, 'attr_name'):
# 获取属性值
value = getattr(obj, 'attr_name')
# 设置属性值
setattr(obj, 'attr_name', new_value)
# 动态调用方法
result = getattr(obj, 'method_name')(args)
可以看到,反射让一段已经变成 0 和 1 的程序,在 CPU 上狂奔的同时,还能抽空"照照镜子",看看"我是谁、我有哪些字段、我能干什么",并且还能动态地调整自己的行为,这种能力为许多高级编程技术(如依赖注入、面向切面编程、序列化/反序列化等)提供了基础。
造一面镜子¶
直观上来说,程序运行时的所有组成部分——代码段、数据段、堆、栈等——都躺在内存中。既然我们的程序能通过指针或引用来读写这些字节,理论上便具备了访问并修改自身状态的能力。再稍微深入一点点,我们还会意识到光看见内存还不够,我们还得能解读内存,比如:
- 我们要知道一个对象在内存中的 布局规则,如字段的偏移、对齐、填充;
- 也要知道一个函数的 入口地址 和 调用约定;
- 以及其他支撑语言抽象机制与内存对应关系的元数据(1)。
反射的核心正是这些元数据(Metadata)。 一个语言或运行时系统是否提供标准化的元数据协议,决定了程序能否安全地解析自身的内存布局(即“看到”自己),并在不破坏自身完整性的前提下,通过操作内存来动态改变其行为。简而言之,只要运行时系统提供了标准化的元数据访问接口(2),造这样一面名为反射的镜子便具备了技术可行性。
- 比如语言支持多态,那么我们还要能找到 虚函数表(vtable) 的位置。
- 即使语言本身不提供元数据支持,也可以人为“补票”。如 Qt 的
moc、protobuf 的反射接口、各种 JSON-RPC 框架的注册宏,都是在 编译期或启动期 显式把元数据塞进全局表里,从而“伪造”出一套反射协议。
Go 语言的镜子¶
总的来说,造一面镜子(反射机制)通常需要维护两类信息:数据位置(用于在内存中准确定位程序元素)和类型信息(用于解释特定内存区域所代表的数据结构及其语义)。在 Go 语言中,这两类信息
