任何足够先进的技术都与魔法无异。——亚瑟·C·克拉克
这句话无论是对于开发者还是非技术人士都有着深刻的意义,在某些时候,对于非技术人士而言可能感受更为真切。回想起自己编程之旅的开端,大约 18 年前第一次学习编程,又在约 15 年后再次踏上学习之路,在这个过程中,这种感受带来了所谓的 “教程地狱”。个人对教程极为反感,总是倾向于亲自尝试构建事物,并且坚信自己所取得的成功在很大程度上要归功于这种方式。
对于开发者来说,教程可能是一种快速入门的方式,但过度依赖教程可能会限制创造力和解决问题的能力。而对于非技术人士,在接触新领域时,可能更容易陷入教程的陷阱,因为他们缺乏相关的背景知识和经验。然而,通过自己尝试构建事物,无论是开发者还是非技术人士都能够更深入地理解问题,培养独立思考和解决问题的能力。这种方式可能会带来更多的挑战,但也会带来更大的成就感和成长。
情况是这样的:
你自认为已然完全掌握了锤子的正确使用方法,懂得如何砌砖、安装石膏板,也学会了用锯子测量并切割梁。然而,当目光投向那些宏伟的建筑物和建筑结构时,却陷入了深深的困惑。这些已然熟悉掌握的工具,究竟是如何被用来建造出如此宏大的结构呢?你茫然不知该从何处着手,只是盯着自己的工具、材料以及其他物资,心中暗自揣测是否存在某种特殊的装备,或者是自己无法获取的秘密知识。你无法理解别人是如何凭借眼前这些同样的工具,达成那样出色的结果。而且,你肯定难以想象自己要如何去切割第一块木板或者铺设第一块砖。
许多人都深知,这恰恰就是学习编程的真切感受。当完全掌握了循环、变量、数据结构、树、栈、链表、数组、控制流等概念之后,再望向编译器、电子游戏、操作系统、浏览器等,心中不禁感叹:“这怎么可能……” 这些开发者一定是从小就学习 C 语言和 x86 汇编语言,并且在斯坦福大学由布莱恩・柯南汉姆亲自传授来自肯・汤普森的知识吧。
假设你不走严格的 JS 框架使用者的道路,随着时光的缓缓流逝,你会开始辨认出一些特定的模式。你会为所使用库的方法调用进行 “去定义” 操作,以此查看其具体的实施方式。同时,你还构建了众多的副项目,观看了大量诸如 “tsoding daily”“sphaerophoria” 以及 “awesomekling” 之类的内容,逐步去阐明网络协议、图像 / 视频编码或者系统调用 / 文件 IO 操作的基本工作原理。倘若必须编写一个 shell 或者 Lisp 解释器,你将不再感到全然迷失。至少你会知晓在开始的时候需要将源文件读入内存,并将其分割成标记,接着再尝试进行解析,构建所需的语法树,以便能够遍历和分析它,进而一步一步地执行代码。曾经那些让你感觉如此显而易见的事物,如今却仿佛成了一门只有特定编程精英才能够掌握的神秘术法。
我坚信,不只是我,很多人在不止一次地揭开某个部分所谓的 “魔法” 时,都会产生这样的想法:
噢哦哦,原来是这样啊。当然,那还能怎么弄呢?真不敢相信我居然没能看出这一点。
随着时间不断推移,越来越少遇到那种让我无法从极为广泛且高层次的心理层面去解析某个实现的情况。虽然我现在并不敢声称自己知晓内核内部、3D 渲染或者 GPU 驱动是如何运作的,但我想说的是,对于大多数事情而言,那种阴暗的神秘感已然消失,它们更像是我可以充满兴奋去学习的东西,而非令人恐惧的被禁止的知识。尽管对于那些方面来说,情况也可能大致相同。
前几日,在漫长的一天管理不同环境 /k8s 集群之后,我如往常一般决定浏览 Hacker News。在这个过程中,我遇到了一篇关于 Go 的 comptime 的帖子,该帖子链接到了 GitHub 存储库上。它瞬间吸引了我的注意力,因为虽然我本人没有亲自编写过 Zig,但是安德鲁・凯利是我的编程偶像之一,我必然会关注他的开发进展。comptime 是 Zig 最让其他语言艳羡的功能之一,尽管通过元编程或者其他语言中的 constexpr 可以实现类似的功能,但 Zig 的直接过程方法 / API 在这方面显得尤其独特,并备受推崇。
就在这时,那熟悉的感受再度出现了:
如果你告诉我需要在不改动编译器的情况下,在 Go 中实现 comptime,那便是我当时的感受。
所以,我决定必须要弄清楚这是如何做到的。于是,我抽出几个小时的时间,尝试贡献一点自己的力量,或者至少添加一些无论何种级别的特征,以此迫使自己理解其中的奥秘。
在短暂浏览代码之后……
原来,通过传递标志在构建时获得的源文件信息,可以实现在 Go 中通过 -toolexec 标志在构建时调用。在这种情况下,调用 prep 二进制程序,该程序带有程序的绝对路径,并使用作者的一个组合包 goinject 和 yaegi(一种优雅的 Go 语言解释器)库。如此一来,你可以获得抽象语法树(AST)、文件装饰器以及导入恢复器。通过实现 Modifier,能够从树中相关的函数收集变量,将它们输出到临时文件中,然后对其进行解释,从而得到 foo 的计算结果 prep.Comptime (foo ())。最后,使用 Modify 传递替换 DST 中的值。就这样,实现了编译时的计算。
噢,对啊。这完全在情理之中。我之前还以为它是通过其他什么神奇的方法实现的呢?
经过几个小时的努力,我添加了变量作用域以及全局常量声明,最后却认定其实这并不是一个有用的功能。因为每个函数都是独立计算的,实际上几乎没有任何命名作用域冲突的可能性。但关键在于,直到写完它并附带了一些测试后才明白这一点。尽管该 “特性” 毫无用处,但在整个过程中却学到了很多,是一次很好的时间利用。
这只是在提醒所有处于不同阶段的开发者,所谓的 “魔法” 并不存在。在绝大多数情况下,你只是缺少必要的背景知识。一旦你掌握了这些知识,一切就都变得合理了。
学习日常工作之外的内容始终是值得的。随着你对基础知识理解的加深,一些其他环节就会自然而然地解开谜团。即便现在觉得这些知识不重要,但我保证在未来的某些时刻,它们一定会有所回报。
每天都努力学习,追求更深层次的理解,花时间去构建甚至是那些 “已解决的问题”。即使你只负责 React 开发,理解其内部工作原理或者一键点击 “服务器端” 自动扩展部署的工作机制也是非常有价值的。
完
领取专属 10元无门槛券
私享最新 技术干货