当前位置: 首页 > 科技观察

我是怎么看代码的

时间:2023-03-23 10:34:49 科技观察

作为一个程序员,总会有厌倦重复工作的时候,羡慕做的这么好的Star项目,Star的数量越来越多。而阅读代码是缓解焦虑的好方法。每当我领略到软件的奇妙设计,惊叹于优美整洁的代码,甚至发现评论中隐藏的彩蛋时,仿佛与作者穿越到了不同的时空,愉快地聊了一会儿。阅读代码很有趣,但通读和理解需要付出努力。这篇文章是我在日常代码阅读中积累的一点经验。分享出来,希望能引起大家的共鸣。1.寻找良师优秀的项目就像一位良师,我们可以从中学到各个领域全方位的知识。但在开始阅读代码之前,最大的问题是:如何才能找到合适的代码项目?Star数高的项目是否更好?从某些角度来说确实如此,但是在gitstar-ranking上,4kStars的Repo只能排到第5000名,至少50kStars才能排到前100。单看stars数量,太多了要选择的项目。在我看来,把握以下几个方向,一般可以筛选出合适的项目:兴趣使然,首先要选择自己感兴趣领域的项目。很多代码片段枯燥难读(比如“飞天”的位运算,莫名其妙的提高性能的语句,或者包含大量隐性知识等),只有自己感兴趣,才会有意愿和动力继续阅读,你能从中找到乐趣吗?有时我们开始阅读代码的机会只是为了在我们的工作中使用它。如果我们想更多地了解它的原理和设计,这是一个很好的起点。想必很多同学都是第一次看源码,因为已经一层层追上了Spring的Class。有时候你可能会觉得某项技术很神奇,就像变魔术一样,你越想不通,就越想知道它是如何“施展魔法”的。简而言之,一旦你有兴趣,你就会想要更多地了解它。但是,如果你读到一半就失去了兴趣,请大胆放弃。丢了这片草地,还有一整片森林等着我们去探索。经典且被大量使用拥有大量用户的经典项目经得起时间的考验,不断迭代,通常在设计上有许多突出的特点。经典项目的维护者一般都是非常资深的工程师,而且还会得到大公司的赞助,保证代码的高质量。这类项目在阅读的过程中可以学到很多知识,包括架构抽象、性能优化、工程化等等。常见的典型项目包括:Go、Kubernetes、MySQL等。适当规模的项目,代码太多,有时名声在外,难免让人望而生畏。实际上有很多很棒的代码库没有很多行。首先是各种语言的标准库,比如Java的Stream和Lock的实现。此外,还有很多小而美的开源项目,比如redis、leveldb等,甚至很多经典大学课程中的Labs都隐藏着优秀的代码,比如xv6。简而言之,这类代码可能几天或几周就可以粗略地读完主体部分,特别适合学习设计思想。2、看完文档,选择项目,我们差不多可以对它有一点了解了。这个时候不要直接clone代码。代码完整的包含了所有的知识,但也毫无保留的暴露了细节。直接进入代码很容易迷路。对于事物的理解,从整体到部分,从抽象到细节是一个比较容易接受的过程。成熟的项目通常都有比较详细的文档,一般分为两类:面向用户的使用文档和面向贡献者的开发文档。了解概览通过阅读文档,我们可以快速了解项目的目的,解决什么问题,从用户的角度看这个软件是什么样子的。除了看概览,我一般也会关注配置。通过所需的配置,您可以进一步了解软件的依赖关系和外部特征。以TiDB为例,其文档截图如下:从左侧边栏可以了解到文档的结构包括介绍、部署、配置、参考等部分。这些部分是用户最关心的内容。架构与模块优秀的开发文档一定会包括整个软件的架构模型和关键模块的设计。通过阅读架构图和高层设计,软件的原理和解决问题的思路一目了然。对于有一定经验的读者来说,看到架构设计后甚至可能知道软件的工作流程。上图是TiDB开发文档的截图。我们发现它不仅包括架构设计,还详细地告诉读者如何开始代码、如何贡献、详细的设计过程等。除了架构设计,相对完整的开发文档还会包含关键模块的信息。关键模块可能涉及到核心逻辑和数据结构的设计,以及边界处的合约和交互方式等。针对Go语言这种不断演进的开源编程语言,甚至还专门建立了一个Proposal仓库来跟踪设计、讨论、编码和发布各种提案,比如等了10年的通用提案:弄清楚架构模型和关键模块,真正打开源代码的时候,包里包含的知识,文件名,界面等都可以映射到整体结构上,在脑海中形成一个完整的画面。其他前置知识有时候文档的作者会加入很多前置知识,比如它基于什么样的算法,它启发了哪些知识,甚至是哪篇论文的想法实现了。这些先决知识对我们的理解会有很大的帮助。通过学习这些知识,我们可以进一步了解软件的详细设计。上图是etcd的github页面。它在显眼的地方使用了Raft共识算法,并链接到Raft算法的主页。如果不理解复制等概念,就如同看没有字幕的外语电影,精彩程度大打折扣。3.再次阅读代码阅读完文档后,就可以开始阅读代码了。为了防止在代码中迷失,我们可以按照几个原则来阅读:从入口开始虽然我们可以通过架构模型和包与文件的关系大致判断出哪些代码是核心代码,但是会更加准确从入口开始符合大脑的思维方式。因为入口代码的工作一般都是先初始化各个模块,然后调用主线程或者启动主服务,这种简单的有条不紊的工作避免了我们一开始就遇到困难,循序渐进的过程使得大脑更容易产生奖励。如图所示,kubelet启动入口的简化主线逻辑非常清晰。以此为起点,可以通过三种方式查看配置详情、创建kubelet详情、启动详情。抓住主线,从抽象到实现,主线就是如何从输入一步步产生输出。在这个过程中,涉及到多个模块,每个模块都有自己的输入输出。当我们按照函数调用和数据传输的方向一步步进行时,随着抽象层次的不断降低,涉及到的细节也越来越多。这个时候,我们应该及时回头。一路不见底,很容易迷失在其中。一个好的设计会有合理的抽象。根据不同的开发语言,我们可以通过查看包、接口、特性、公共方法列表、头文件等快速获取抽象信息,逐步拼接出程序的主线。理清主线后,逐步展开抽象,阅读具体的实现代码。还是以kubelet为例。kubelet作为负责整个节点运行的核心,任务繁多且复杂。但是看它的代码分包结构,把不同的功能点分到不同的目录下,还是很清晰的。结合初始化逻辑,再深入到各个函数目录,可以发现kubelet的模块设计遵循多管理器围绕,核心协作模型。一个好的抽象,就像洋葱一样,有不同的层次。记录初识一个项目,边看边写边画很重要,不会对结构和流程有清晰的把握。有时候跳转很多,之前看过的东西后面忘记了,所以对于关键路径,记录下具体的函数名和模块名,可以帮助我们快速回溯入口。有时你会遇到需要扩展的知识盲点。为了不打断主线思路,可以先记录下来,另找时间研究。另外,当你遇到难以形成概念的不直观的代码表达时,一遍又一遍地阅读也无法理解。这时候就需要画图帮助理解了。一个典型的例子就是在学习B+Tree的拆分、合并、上下移动的时候,完全看代码是不直观的。看懂这种内容画图是很有效的:必要时用debug有一些代码出于正确性、性能等方面的考虑,其表达可能比较费解。人类的思维方式偏向于秩序。用软件开发来类比,我们更容易理解HappyPath而忽略分支细节。当你想不通某段代码为什么要这样写的时候,运行一次,加个断点调试一下,说不定就能找到真正的原因。一个有趣的例子是:在环形队列中,判断队列是否为空是看头指针和尾指针是否重合。下图代码来自一个无锁环形队列的空判断实现。理论上来说,循环队列进入tail++,退出head++。队列先入,队列后出,所以尾部一定比头部大。那为什么在上面的代码中,除了判断tail-head==0,还必须认为tail