最近准备好好的补一下 Go
的基础内容,正好反射这一点也是我比较关注的。
之前在 clash
那里发的 issue 就遇到了这样一个 golang
经典陷阱:Typed nil
和 nil
并不相等,使用 xxx != nil
会得到永真的结果。这个问题的暂时解决方案就和反射有关:
当然了,这样的解决方案是暂时的,我们不能在每个这样的场合都通过反射来解决问题,但这勾起了我对 Go
反射的兴趣。而想要了解反射,reflect
包给了我们一个不错的建议。
读完全文,深有感触。正所谓想要学好反射,不仅在于找准反射之道,关键在于找准反射之道。那我们就开始吧(
简介
无论你听说过反射或否,在这里我们简单的给出一个定义:反射是在运行时获取/修改对象的一种手段。我们知道Java
的反射,我们可以获取、修改 private
,甚至是 final
的内容,这一点对于 golang
来说也不例外。只是与 Java
的 class
机制不同,golang
的类型是零散的,因此无法做到像 Java
那么“动态”。
类型(Type)
对 Go
的类型,相信大家都很熟悉了。在平时使用的过程中,我们已经习惯了这样的使用:
正如最后一行注释所问,B
真的是 AAA
的别名吗?答案是:是、但也不是。这里我们先解释“不是”的原因,而“是”的部分则留给下文来细细阐述。
我们知道,如果定义了一个类型的“别名”,那这时候,想要把它直接赋值给原类型是无法隐式做到的,你必须显式地完成这一转换过程。那是因为 Go
是一门静态类型的语言,而别名前后的类型是不一样的。从这个角度来讲,B
并不是 AAA
的别名,它只是和 A
结构相同的兄弟(きょうだい)而已。
前置知识
接口(interface)
在介绍 reflect
之前,我们先提出一个问题:你们认识中的 interface
是什么样的?
不少人可能会带有 Java
遗留下来的固有印象:interface
是那个 implements
的东西,在 golang
里不需要 implements
了,所以更加灵活。
这样的理解,怎么说呢,之前我也是这么认为的,各个语言的 interface
总该差不多吧。但在读完这篇文章之后,我发现我错的离谱。
Go 中的 interface 是什么呢?
实质上,它只是一个方法的集合。在接口中定义了一定数量的方法(数量也可以为0),而实现了这些方法的类型称作实现了这个接口。
有一种说法认为,Go
和 Java
接口的区别在于 Java
需要显式地声明实现,而 Go
是隐式的。我认为这种说法是表现上的事实和事实上的误导。我们不妨做一个实验:
在这个例子中,我们定义了两个 interface
和一个 struct
。大家可能发现了,B
其实是包含了 A
的,这个例子的输出也一点都不出乎意料。那通过这个例子可以表现出什么呢?
它揭示出了这样的一个事实:Go 的接口实质是方法的集合,只要方法定义相同,那它们就是相同的。
在这个基础上,我们再回来看这样一个特殊的类型:interface{}
。是不是有点感觉了?这其实就是一个没有任何方法的接口,从方法的集合角度来看,这就是一个方法的空集。正因为空集是所有集合的子集,因此任何接口类型都可以转换为 interface{}
类型。
这样的结构也决定了 Go 的接口是真正意义上的“接口”:只要两个口子使用的标准一样、尺寸一样、参数一样,那它就是一样的,而无关生产厂家(声明在何处,以何名称声明);所有生产出来的产品(Type),只要它符合这个标准,那它就可以说是这个标准下的“同类产品”。
说完了这件最重要的事情,最后补充一点大家都知道的吧。接口是静态类型的存在;接口内存储的是指针类型;接口存储的类型是其定义类型或者 nil
。
反射
终于,我们到了本文的主体部分,但某种意义上也是最为简单的部分(当然前提是上文的内容你都理解了)。
如果按照那篇博客的方式来讲的话,它将反射之道归结成了三条规则;而在本文,我们希望摆脱这样的束缚,因为这三条规则并不平衡。我们把它们罗列出来:
- 反射使我们能够通过接口得到反射对象。
- 反射使我们能够从反射对象返回到接口。
- 要修改的反射对象必须是可修改的。
从这三条来看,其实前两条更像是“设计准则”一样的存在,和第三条相比,其重要性完全不同。因此,这里我们不完全遵循原文的讲解思路。我会用我理解的重要程度对反射作出描述。
接口的值(Value)
不知道大家还记不记得上文留下来的这个问题:
B
真的是AAA
的别名吗?
上文中,我们回答了“不是”的原因,这里我们来回答“是”的理由。观察如下代码:
这段代码的输出是什么呢?
float64
这就涉及到了反射中的一个概念:Kind,以及我自认为这门语言最浪漫的地方。
如果你去看 Type 的定义,你会看到这样的一段内容:
第一行的注释清晰地告诉了我们一个试试:下面罗列的所有 Kind
,就是这门语言中所有的真实类型。不论你具体如何书写,到最后都可以表现为这些元素的相互运算。这也是反射之所以能够存在的原因,正因为这个世界是可知的,我们才可以以这样仿佛“动态”的方式去完成我们想做的任务。
所以这时候如果你问我它是别名吗?我想,答案就如同上文所讲的那般不确定了。是也不是,这是对这个问题最好的回答。
可以改变的极限(CanSet)
这条对应的就是那三条中的第三条,因为这条确实重要,因此我也遵循原文的思路讲起。
我们知道,反射的一大作用就是动态修改。但有些东西是不能修改的。拿 C
来举例子吧,C
中的字符串字面量就是无法修改的,其在编译时就确定了。Go
虽然和其不大一样,但也存在反射无法修改的东西。
回顾我们学习的编程语言,赋值是基础中的基础。在赋值时,任何一本教材都会这样教给我们:[Variable Name] = <ToAssign>
,而反之则不一定成立。这是因为 <ToAssign>
可能为数字,而单纯的修改数字“字面量”是非法的。
到了 Go 中,又多了一种情况:试图修改非指针的类型。这里我们拿原文的例子:
这里会抛出 panic
的原因很简单:x
不是指针,reflect.ValueOf
获得的其实和直接在 x
处写 3.4
的效果是一样的。如果要判断某种情况下是否可写,可以通过 v.CanSet()
完成。
那什么内容是可写的呢?内存中的某一段是可写的。因此这就需要我们在传入 ValueOf
时传入一个指针,然后修改其指向的内容。在 Go
中,通常是这样完成的:
最后摘一句原博中的话吧:
Reflection can be hard to understand but it’s doing exactly what the language does, albeit through reflection
Types
andValues
that can disguise what’s going on.
但对我而言,我觉得这句话可以改成:
Reflection is romantic because it’s doing exactly what the language does. Through reflect Types and Values you can know exactly what’s going on.
それではww