这篇文章最初是我 2019 年写的英文版本:Learn from Source Code (an Effective Way to Grow for Beginners)
后来陆续到一些读者的积极反馈,所以我最近把这篇翻译为中文,在翻译的过程中我又顺便重写和简化了一部分。
为什么要读代码
工作多年后,我觉得自己很多时间花费在了阅读代码上。能高效地理解代码是一个程序员的核心技能,这种能力与具体的技术栈没有关系,而是一种通用的、可迁移的能力。
我们教编程的方式注重在写代码的技能,而不是如何读代码。这里的“读代码”指的是刻意地、有目的地去阅读源代码。
编程和写作有很多共同点,我们通过文字或代码表达想法,Donald Knuth 之前还倡导过文学编程的编程范式。
还记得我们在学校是如何学习写作的吗?从小学开始开始,我们需要阅读各种优秀作家的文章,并从中学习各种写作技巧。读书破万卷,下笔如有神。
和看书一样,有目的地阅读代码会帮助程序员更快地成长 (尤其是对于初中级程序员)。
刻意读代码至少有三个好处:
站在巨人的肩膀上
读史使人明智,读诗使人灵透。
好的源代码就像一部文学杰作,不仅仅包含信息和知识,也是好的启蒙。在 Linux Kernel、Redis、Nginx、Rails 这些伟大的开源项目中,可以找到无数优秀的编程技巧、范式选择、设计架构,阅读这些代码你可以汲取全球顶级程序员的技巧和智慧。
读源码的另一个好处是能够避免常见的陷阱,因为软件开发中的大多数错误已经被其他人犯过。
解决难题
在你的整个编程生涯中,肯定会遇到无法通过谷歌搜索解决的问题,阅读源代码通常是解决这类问题的好方法,这也是学习新东西的好机会。
拓展见识
大多数程序员只是工作在几个特定领域。一般来说,如果你不持续地逼迫自己拓展知识面,你的编程能力将趋于同事的平均水平。
作为程序员,如果要持续成长就要不断尝试自己感兴趣的新领域,从深度和广度拓展对编程的理解。
读什么代码
现在有这么多优秀的开源代码可供选择,我们应该阅读什么样的源代码?
我们通常是带着目的去阅读源码的,那么有以下是几个典型场景:
要学习一门新的编程语言时
学习一门新的编程语言不仅仅意味着学习语法,更需要学习如何完成一些常见的编程任务。这需要读一些小型项目,比如 Learn xxx By Examples 之类的。
我从 rust-rosetta 项目中学到了很多关于 Rust 的知识,Rosetta Code 收集各种编程语言中常见任务的示例代码,这是学习新编程语言的有用资源。
要理解具体技术的实现时
我们都使用了标准库中的 sort 函数,你有没有想过它是如何实现的?或者说你需要在 Redis 中使用 Set 数据结构,它的实现中用到了哪些数据结构?
那你需要读标准库的源码或者 Redis 的源码,通常我们关注的是几个文件或函数。
当你比较熟练使用某个框架后,可以尝试去阅读框架某些组件的源代码,这样可以加深对框架的理解。
要学习一个新领域时
这时候适合阅读该领域的经典项目,不要选择太大的项目,可以从优秀的开源课程开始。
假设你想学习分布式系统,MIT 的分布式课程是很好的课程,里面也有几个经典的大作业。如果你了解 Golang,那么 etcd 可能是个不错选择。
你想深入了解操作系统的内部实现吗?直接读现在的 Linux Kernel 版本肯定会让新手摸不着北,那么 Xv6 或者 Linux Kernel 的早期版本会是一个好的开始。
想学编译器实现?也许可以看看 rui314/9cc。
在 Github 上拥有许多好的的教学性质的开源项目,也有一些重造轮子类的项目,可以尝试 “tiny + 关键词” 或者“make your own + 关键词”这样来搜索。
根据你当前的技能和知识水平选择项目。如果你选择的项目远高于你当前的技能水平,结果会迷失在源码中而受到打击,那么先阅读一些较小的项目,然后继续读较大的项目。
如果你花了一段时间都无法理解代码,这通常意味着你欠缺背景知识,那么先把代码放在一边,试着阅读一些书籍、论文或相关文档,等有信心了再回来。
比如我如果一股脑直接去看 Raft 的代码就会很吃力,所以我得先看 Raft 配套的文档和论文。
真正的成长,是始终游走在“舒适区边缘”。我们总是以这样的模式取得进步:阅读(代码、书籍、论文)、写代码、读更多、写更多。
如何阅读源代码
阅读代码并不容易,我们得试图理解代码中的设计和思想,需要比较长时间的精神专注。为了有效地阅读代码,最好准备以下这些技能和工具:
前置准备
能够高效地使用编辑器,例如快速搜索关键字、查找变量和函数的相关引用。最好能对编辑器熟悉到仅使用键盘来操作,这将使你专注于代码而不会中断思维。
基本掌握 Git 或其他版本控制工具,比如比较不同版本之间的差异。
找到所有源码相关的文档,尤其是设计文档、代码约定等。
对所用编程语言有所了解,如果是阅读大型项目,需要了解设计模式。
当然,这些也需要在常年地读写代码中积累经验,保持耐心。
流程和技巧
读代码的过程和读书有些差别,读书我们通常按照章节的线性顺序去读,如果读代码也是这样从头读到尾则容易瞌睡,而且效果也不好。
大多数时候我们根据项目的组织,自顶向下、或者自底向上地读代码,很多时候我们的注意力只是专注在少数源文件上。
以下是一些更有效地阅读代码的技巧:
带着问题阅读代码
当你开始阅读代码时,可以尝试抛出一些问题。例如,一个应用程序有一个缓存策略,一个很好的问题是如果缓存失效会发生什么,缓存中的值是如何更新的?
这样就能在心中确定一个目标,你也可以对自己问题做出一些猜想,然后根据代码验证猜想,这有点像侦探:你想理解代码的真相和逻辑,这就像是找一个故事的真相。
读代码的经验多了,就能不断抛出各种问题,引导自己不断地挖掘代码,最终的阅读顺序倒不重要了,因为我们是随着自己的好奇心读懂了整个项目。
运行和调试代码
写代码的过程有点像搭积木,而已经完成的代码就像一个组装好的乐高。
如果你想了解它是如何组合在一起的,一个好的办法是沿着提交记录来理解,这时候版本控制工具就很有用了。
假设我想看某个特定功能是如何实现的,我可以根据提交日志,尝试读 Git 的提交记录。我发现 Lua 的第一个版本要简单得多,这有助于我理解作者最初的设计思想。
调试是另一种理解代码的方式,可以尝试在代码中添加一些断点(或打印语句),把代码运行起来。如果对代码有足够的了解,我们也可以尝试进行一些修改,最简单的是尝试调整配置看看效果,然后逐步尝试添加一些小的功能,如果结果对其他人也有用,可以顺便做些开源贡献。
画出代码里的关系
“糟糕的程序员担心代码,优秀的程序员会担心数据结构和他们的关系。”
– Linus Torvalds
在读代码的过程中,用笔或任何工具画出数据结构、模块之间的关系、主要的流程图等。这就像是源代码的地图,在阅读过程中你可能需要经常参考这些地图和索引。
scitools 等一些工具可用于自动生成 UML 图,我最喜欢的画图工具是 Excalidraw,ProcessOn。
注意模块和边界
大型项目中通常包含多个模块,在设计良好的项目中一个模块通常具有单一职责,它的变量和函数以一种可读的风格命名,这也使得代码更容易维护。
模块的接口是抽象边界,我们可以忽略掉那些我们暂时不关心的模块。和《如何阅读一本书》中介绍的精读和泛读一样,自己感兴趣的部分精读,其他部分则泛读,这将大大节省整体时间。
模块也不止是按照目录来组织的,如果你正在阅读使用 GNU Make 构建的 C/C++ 项目,Makefile 将是了解模块组织方式的一个很好的入口。
使用测试用例
测试用例也是理解代码的一个很好的补充,我认为测试用例也是一种文档。像 Rust 就很好,测试和实现通常在一块。
如果读一个类,试着读一下相关的测试代码,这可以让你弄清楚一个类的接口以以及它的典型用法。集成的测试用例对于调试带有某些特定输入的代码也很有用,它可以让你跟踪程序的整体流程。
回顾和总结
花了很长时间读一个项目,为什么不写篇文章阐述一下自己的理解?
这就像在写读书的读后感,你可以写下源代码中的好坏,以及你从中学到的新东西。教授别人是一种最好的学习方式,写这样的文章会加深你的理解,也有助于其他人阅读源代码。
我当初在花了好一段时间阅读 Kong 的源码后,写了一系列关于 Kong 的文章:Kong 源码分析
总结
写了这么多,我发现代码阅读比想象的要复杂得多,没有标准的、系统的方法来训练这项技能。总而言之,不断练习读代码,找到适合自己的方法和工具,读得越多就会越快、越高效。
最后推荐两本提高读代码能力的好书:
如果喜欢这篇文章,记得分享、点赞 👻