Golang 的反射

文摘   2024-10-10 16:40   上海  

_ Dolt[1] 是一个用Go语言编写的具有分支和合并功能的数据库,这篇文章是我们众多 与Go相关的博客文章[2] 之一_

尽管我已经从事编程工作20多年了,但我从来不是一个编程语言爱好者。我不会为了好玩而去寻找新的语言。在亚马逊、谷歌和 其他一些有聪明人的地方[3] 工作过,我听过开发人员对他们特定选择的语言发表的各种观点。更快!更安全!更动态!这些通常是非常坚定的观点,会引发大量的争论和激烈讨论。就我而言,我通常会试图找到一种方法来避开这种对话。

我是一个实用主义的程序员。我通常只使用任何语言中最普通的功能。如果我在某个编程任务中取得了成功,那么5年后可能会有某个可怜的人来维护这些代码。所以我倾向于避免深入研究那些深奥的东西。除非我打破了自己的规则...

有时确实需要做一些粗糙且难以维护的事情。比如有一次我需要在C程序中使用void**来传递匿名函数,然后将它们转换成我认为必要的任何东西。那是在一个"安全"产品中。真可怕。还有一次我使用JNI访问特定操作系统的系统调用以获得性能提升。那肯定给那个程序的可移植性钉上了棺材钉。

这就引出了Golang的 reflect[4] 包。几周前,作为一个实验的一部分,我需要恶意地修改Dolt数据库中的一段数据。由于我从未使用过Go的reflect包,我想我可以试一试。由于不太了解,我掉进了反射的池子里,几乎永远迷失在一个平行宇宙中。AI对这种体验的梦幻描绘非常准确。我感觉不再有上下之分,内部就是外部。

狗是我的副驾驶

像往常一样,我开始编写一些代码来弄清楚如何使用reflect包。生活在 Copilot[5] 和 其他生成式AI[6] 工具的时代,一些看起来相当可信的代码弹出在我的文本编辑器中,我就开始了。

bsVal := reflect.ValueOf(blockStore).Elem()

tables := bsVal.FieldByName("tables")

typ := tables.Type()
fmt.Printf("tables.Type: %v\n", typ)
for i := 0; i < typ.NumField(); i++ {
fmt.Printf("tables %d: %s\n", i, typ.Field(i).Name)
}
for i := 0; i < typ.NumMethod(); i++ {
fmt.Printf("method %d: %s\n", i, typ.Method(i).Name)
}

这段代码做的事情并不特别有趣,实际上也不是重点。重点是它看起来很合理,特别是对于不了解reflect设计模式的人来说。对我来说,它看起来像是要获取blockStore并开始通过打印其字段和方法来检查它。

很酷的故事,但实际上它立即就崩溃了。通过将鼠标悬停在FieldByName方法上,我可以很容易地看到文档,文档非常清楚地说明,如果给出错误的输入,它就会崩溃。这一点立即就很清楚,但是在我理解其基本原则之前,我仍然难以使用reflect库。

反射法则

如果你要使用reflect,你必须阅读 Rob Pike的反射法则[7] 。我承认我花了比应该花的更长的时间才找到它,但也许这里的教训是,你那个带有所有花哨AI功能的神奇IDE并不能替代阅读文档。

如果你拒绝阅读Pike的法则,那么我别无选择,只能把它们强行放在你的眼前:

  1. 反射从接口值到反射对象[8]
  2. 反射从反射对象到接口值[9]
  3. 要修改反射对象,该值必须是可设置的[10]

前两条法则看起来并不太有争议。ValueOf()Interface()方法基本上是在值和反射对象之间进行转换。Rob甚至对这两条法则进行了双重总结:

简而言之,Interface方法是ValueOf函数的逆操作,只是其结果总是静态类型interface{}。
重申一下:反射从接口值到反射对象,然后再返回。

第3条法则对我来说更难接受。我想让main包中的代码调用另一个包中未导出的方法,但事实证明这是不可能的。对于我正在处理的特定问题,这最终成为了一个无法开始的方案,我不得不重新考虑。话虽如此,这足以让我接触到reflect包,以至于我对它有了一些看法。

缺失的法则

当人们开始讨论热力学时,有一个普遍的假设没有被提及,直到后来, 第零定律[11] 才出现。

Go的作者们kindly给了我们三条reflect法则,但我认为他们忽略了提及第0条法则,它应该是:

  1. 使用reflect需要自担风险。误用它,它会毫不留情地panic

我认为这个设计决定,即在任何稍微出错的情况下就panic,是一个非常清晰响亮的声明,表明Go的维护者不希望你使用reflect包。这是一个天才的举动,让它从一开始就感觉有毒。

不过这不仅仅是panic的问题。在文档中多次提到了 unsafe[12] 包,并且在几个接口中使用了unsafe.Pointers。我认为进一步阻止使用reflect包的唯一方法就是将每个方法都标记为废弃。

我认为可以肯定地说,任何在生产中使用reflect的行为都应该受到高度审查,并且要非常小心。这并非不可能。任何使用 encode/json[13] 的人都已经上瘾了。

废除第三条法则

我第一次使用反射是在Java中,java.lang.reflect能够更新你想要的几乎任何值。当与自定义ClassLoader和热安装 ByteCode[14] 的能力结合使用时,你就拥有了一个真正强大(危险)的工具。在面对java.lang.reflect时,Java中的private字段实际上没有任何意义。

在Go中,情况并非如此。你可以读取任何字段,但只能更新导出的字段(以大写字母开头)。对于结构体上的方法,如果它们没有被导出,你甚至无法了解它们的存在。然而,你可以调用一个导出的方法,该方法可能会对你本来无法修改的数据进行写入。

可能有更深层次的原因解释为什么无法更新未导出的字段,比如编译器剥离了一些使其无法实现的元数据。撇开这一点不谈,考虑到reflect包不适合在生产中使用(参考第0条法则),我们应该能够更新任何字段并调用任何方法,即使它没有被导出。在每个角落都潜伏着panic的威胁的情况下,无法接触到内部字段的安全性似乎是不必要的。我已经得到了一把猎枪可以打掉我的脚,但我没有子弹。

如果能够在reflect中完全自由地进行写操作,就有可能构建更广泛的测试和调试工具。在Dolt的特定情况下,我们试图破坏存储格式作为常规测试的一部分,而这些代码我们不希望存在于Dolt代码本身中 - 因此走上了这条道路。更普遍地说,它可以实现比现有行为更广泛的工具集。

至少,我们可以在JSON结构中编码未导出的字段。那不是很好吗?

结语

reflect不适合胆小的人。我认为这正是维护者想要传达的观点,他们已经成功了。很难抵制住在reflect周围放置一些漂亮的包装代码的冲动,但最终它对于5年后清理我的烂摊子的可怜人来说将是不可能阅读和维护的。所以我就会避免使用它,正如Go之神所希望的那样。

在这个分歧的时代,我们能否都同意新的从零开始的法则?仍然是三条法则,只是适当地从0开始。来 Dolt Discord服务器[15] 告诉我们我们错了吧。

参考链接

  1. Dolt: https://github.com/dolthub/dolt
  2. 与Go相关的博客文章: https://www.dolthub.com/blog/?q=golang
  3. 其他一些有聪明人的地方: https://www.linkedin.com/in/macneale/
  4. reflect: https://pkg.go.dev/reflect
  5. Copilot: https://copilot.microsoft.com/
  6. 其他生成式AI: https://supermaven.com/
  7. Rob Pike的反射法则: https://go.dev/blog/laws-of-reflection
  8. 反射从接口值到反射对象: https://go.dev/blog/laws-of-reflection#1-reflection-goes-from-interface-value-to-reflection-object
  9. 反射从反射对象到接口值: https://go.dev/blog/laws-of-reflection#2-reflection-goes-from-reflection-object-to-interface-value
  10. 要修改反射对象,该值必须是可设置的: https://go.dev/blog/laws-of-reflection#3-to-modify-a-reflection-object-the-value-must-be-settable
  11. 第零定律: https://en.wikipedia.org/wiki/Zeroth_law_of_thermodynamics
  12. unsafe: https://pkg.go.dev/unsafe
  13. encode/json: https://pkg.go.dev/encoding/json
  14. ByteCode: https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
  15. Dolt Discord服务器: https://discord.gg/gqr7K4VNKe

幻想发生器
图解技术本质
 最新文章