🗒️初探Go反射三大定律
00 分钟
2021-10-13
2023-9-26
type
status
date
slug
summary
tags
category
icon
password
😀
最近在研究Go语言的源码,看到反射部分,结合The Go Blog系列的《The Laws of Reflection》,以及Go 1.15 中 src/reflect 部分源码,记录下对于Go 反射的一些见解。

〇、前情提要

Go的反射基础是接口类型系统。学习之前,最好先了解Go接口的实现,另外,反射的API也很多,了解其核心部分即可,一些其他API可以在通过源码分析来了解。笔者在本文中只是结合源码分析初探Go反射的三大定律。
notion image
在反射的世界里,我们拥有了获取一个对象的类型,属性及方法的能力。

一、反射的基本概念

什么是反射(reflect)
维基百科如是说明
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
在计算机科学中,反射式编程或反射是一个过程检查、内省和修改自身结构和行为的能力
另外还配上了Go语言的一些示例代码
今天我们重点讲讲Go语言的三大反射定律
为什么要使用反射?
  • Go 不支持泛型,通过反射可以间接实现泛型的需求
  • 借助反射可以极大简化设计,不需要对每一种场景做硬编码处理
  • 反射提供了一种程序了解自己和改变自己的能力,这为一些测试工具的开发提供了有力的支持。

二、反射的两种基本数据结构

Go的反射巧妙地借助了实例到接口的转换所使用的数据结构,首先将实例传给内部的空接口,实际上是将实例类型转换为接口可以表述的数据结构emptyInterface,反射基于这个转换后的数据结构来访问和操作实例的值和类型。实例传递给interface{} 类型,编译器会进行一个内部的转换,自动创建相关类型数据结构。

2.1 reflect.Type

源码见src/reflect/type.go
可以看到,Type是一个接口,它里面定义了28个方法
为什么反射接口返回的是一个Type接口类型,而不是直接返回具体的类型结构呢
  • 一是因为类型信息是一个只读的信息,不可能动态地修改类型的相关信息,那太不安全了;
  • 二是因为不同的类型,类型定义也不一样,使用接口这一抽象数据结构能够进行统一的抽象。

2.2 reflect.Value

源码见src/reflect/value.go
可以发现,Value是一个结构体,它包含三个字段
  • typ 值的类型指针
  • ptr 指向值的指针
  • flag 标记字段
另外,Value 还有68个方法,这里先就不赘述了,包括61个 public 方法和7个 private 方法

三、反射三定律

The Laws of Reflection》原文提到反射的三个定律
  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.
翻译过来就是
  1. 反射可以从接口值得到反射对象
  1. 反射可以从反射对象对到接口值
  1. 若要修改一个反射对象,则其值必须可修改
是不是感觉听起来很绕,我们一一解读。

3.1 第一定律

📌
反射可以从接口值得到反射对象
从接口对象获取对应的反射对象可以使用reflect.TypeOf()reflect.ValueOf()分别获取反射的类型对象与反射的值对象。也就是第二节中的 reflect.Typereflect.Value
notion image
我们看一个例子:
  • TypeOf()
  • ValueOf
可以看到两个函数都是传递的一个空接口类型的值,参数为空接口时,可以接受任何类型。所以这里传入的类型可以任何类型的值。关于空接口的知识点不在本文中阐述。

3.2 第二定律

📌
反射可以从反射对象对到接口值
刚好和第一定律相反。
notion image
Value 有方法 Interface() 支持从 reflect.Value 类型 转为 接口变量。
另外,还提供了丰富的方法来实现从 Value到 接口对象实例的转换。
注意:Type是不支持逆向的,因为里面只包含类型信息,所以无法逆向转化。
我们看一个例子:
如果想要获取最初的类型,可以用类型断言进行转换。

3.3 第三定律

📌
若要修改一个反射对象,则其值必须可修改
这里提到一个可修改的概念,也就是settable
首先,我们应该了解,在Go中所有的传递都是值传递。值变量传递拷贝的值,指针变量传递时的指针地址的拷贝。
Value 值在什么情况下是可以修改?我们知道接口对象传递给接口的是一个完全的值拷贝,如果调用反射方法reflect.ValueOf() 传进去的是一个值类型变量,则获得的Value实际上是原对象的一个副本,这个Value是无法被修改的。如果传进去的是一个指针,那么Value是可以修改的。
Value 值的修改涉及如下两个方法:
CanSet() 可以确定一个 Value 是否可以修改
Set() 方法用于修改 Value,另外还有其他不同类型的Set方法
notion image
我们看一下例子:

四、reflect 转化常用API

到此,我们已经了解了反射的三大定律。我们来总结下
下图是接口对象、Type、Value之间的转化关系以及使用到的API:
notion image
图中提到的一些 API
  1. 从实例到Value
    1. 通过实例获取 Value 对象,直接使用 reflect.ValueOf()
  1. 从实例到Type
    1. 通过实例获取反射对象的Type,直接使用 reflect.TypeOf()
  1. TypeValue
    1. Type 中只有类型信息,所以直接从一个Type接口变量里面是无法获取实例的Value的,但是可以通过该Type构建一个新的实例的Value。
      如果知道一个类型值的底层存放地址,则还有一个函数可以依据type和该地址值恢复出Value的。
  1. ValueType
    1. 从反射对象 Value到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到Type类型的指针。
  1. Value 到实例
    1. Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value到实例的转换
  1. Value 的指针到值
    1. 从一个指针类型的Value获取值类型Value有两种方法
  1. Type 指针和值的相互转换
    1. 指针类型 Type 到值类型Type
      值类型Type到指针类型Type

五、深入挖掘rtype

首先我们看下一个最通用的类型公共信息 rtype,它是每一种基础类型的一个成员类型。
如果有同学看过 runtime 的源码,那么可能知道,rtype_type 是同一个结构体
它实现了 reflect.Type接口
notion image
可以看到,其他的基本类型都有一个rtype 类型的成员变量
关于Type类型中的主要方法
  1. 所有类型通用的方法:
  1. 不同基础类型的专有方法
      • Int*Uint*, Float*, Complex*: Bits
      • Array: Elem, Len
      • Chan: ChanDir, Elem
      • Func: In, NumIn, Out, NumOut, IsVariadic.
      • Map: Key, Elem
      • Ptr: Elem
      • Slice: Elem
      • Struct: Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField
如果调用错误,那么会panic
Go 定义了26个基本类型
一些常见基本类型的Type 实现
  • arrayType
  • chanType
  • funcType
  • interfaceType
  • mapType
  • ptrType
  • sliceType

六、反射的优缺点

6.1 优点

  1. 通用性
    1. 特别是一些类库和框架代码需要一种通用的处理模式,而不是针对每一种场景做硬编码处理,此时借助反射可以极大简化设计
  1. 灵活性
    1. 反射提供了一种程序了解自己和改变自己的能力,这为一些测试工具的开发提供了有力的支持。

6.2 缺点

  1. 反射是脆弱的
    1. 由于反射可以在程序运行时修改程序的状态,这种修改没有经过编译器的严格检查,不正确的修改很容易导致程序的崩溃
  1. 反射是晦涩难懂的
    1. 语言的反射接口由于涉及语言的运行时,没有具体的类型系统的约束,接口的抽象级别高但实现细节复杂,导致使用反射的代码难以理解
  1. 反射有部分性能损失
    1. 反射提供动态修改程序状态的能力,必然不是直接的地址引用,而是要借助运行时构造一个抽象层,这种间接访问会有性能的损失

6.3 Best Practice

  1. 库或框架内部使用反射,而不是把反射接口暴露给调用者,复杂性留在内部,简单性放到接口
  1. 框架代码才考虑使用反射,一般的业务代码没有抽象到反射的层次,这种过度设计会带来复杂度的提升,使得代码难以维护
  1. 除非没有其他办法,否则不要使用反射技术

📎 参考文章

  1. The Laws of Reflection
  1. src/reflect
  1. Go 核心编程
 
上一篇
【笔记】Redis数据结构与对象《Redis设计与实现》
下一篇
【翻译】《A Quick Guide to Go's Assembler》

评论
Loading...