不要把源码当书读,把它当案发现场读。
读源码不是从第一页读到最后一页,而是在回答一个具体问题:
- 这个请求从哪里进来?
- 这个状态是谁改的?
- 这个接口为什么这样设计?
- 这层抽象到底保护了什么?
如果没有问题,源码就只是一大片文字;有了问题,源码才会变成线索。
先找入口,不先理解全局
阅读大型代码库时,第一目标不是“理解整个系统”,而是找到一条真实入口。
入口可以是:
- 一个 public API。
- 一个报错栈里的函数。
- 一个测试用例。
- 一个真实调用点。
- 一个配置项最终生效的位置。
比如读一个 observability 模块时,不应该一上来读完整个目录,而是先找一句真实调用:
executeWithContext({ span, fn });这句调用比目录结构更诚实。目录结构告诉你代码被放在哪里,真实调用告诉你系统正在怎样运行。
只追一条链路
一次只追一个问题。
例如问题可以是:
一次业务调用是怎么变成一个 OpenTelemetry child span 的?
这时就只追这一条链路,不要同时试图理解 agent、workflow、tool、storage、Studio UI 和所有 exporter。
源码阅读最怕贪。贪会让人每个文件都看过一点,但每条链路都没有真正闭环。
更好的做法是:
- 找到入口。
- 顺着调用链往下走。
- 遇到分支先标记,不立刻展开。
- 追到结果或边界为止。
- 回头补关键分支。
读源码不是扫地图,而是追踪一条脚印。
读边界函数,而不是读所有函数
不是每个函数都同等重要。优先读边界位置,因为边界最容易暴露系统设计。
重点看这些地方:
- 构造函数:对象如何被初始化,依赖在哪里注入。
- public API:外部世界如何进入系统。
- adapter / exporter / bridge:系统如何连接上下游。
- interface / type:作者认为哪些能力必须稳定。
execute/run/start:真正执行动作的地方。- 错误处理:系统认为哪些失败重要,以及是否用结构化错误保留底层原因。
- 测试用例:作者希望哪些行为不被破坏。
边界函数通常比内部工具函数更值得先读。内部函数解释“怎么做”,边界函数解释“为什么系统要这样分层”。
把代码翻译成因果句
源码阅读不能停在“这里调用了 A”。这只是复述代码,不是理解代码。
要把代码翻译成因果句:
- 因为有
currentSpan,所以不会开新 trace,而是挂 child span。 - 因为
wrappedFn被放进AsyncLocalStorage,所以日志能读到当前 span。 - 因为 exporter 只负责事后发送事件,所以 HTTP / DB 自动插桩的父子关系必须在执行期间由 bridge context 处理。
- 因为测试覆盖了这个回归场景,所以这段看似绕的初始化逻辑不是随手写的,而是在防止打包 tree-shaking 后丢失副作用。
因果句会逼你回答“为什么”,而不是只记住“发生了什么”。
测试是作者给你的地图
不懂实现时,先搜测试。
测试通常会告诉你三件事:
- 这个设计在防什么 bug。
- 哪些行为被认为是契约。
- 哪些边界情况不能被重构破坏。
尤其是回归测试,它往往直接暴露“这里曾经出过什么问题”。
读测试时,不要只看断言是否通过,要问:
- 为什么作者专门写这个 case?
- 这个 case 如果失败,用户会看到什么问题?
- 它保护的是 public behavior,还是内部实现细节?
源码阅读要有输出物
读完源码以后,不能只剩一句“我看过了”。
每次读完,至少写五行:
问题:我想搞懂什么?
入口:我从哪个 public API / 调用点进来?
关键对象:这条链路里有哪些核心对象?
调用链:A -> B -> C -> D
关键分支:什么时候走 child span?什么时候开 root span?
遗留问题:还有哪里没懂?输出物不需要长,但必须能让未来的自己重新进入理解现场。
没有输出,源码阅读很容易变成一种熟悉感幻觉:读的时候好像懂了,过两天只剩“我知道我读过”。
一个实用阅读流程
可以把一次源码阅读压缩成这个流程:
- 写下问题。
- 找一个真实入口。
- 追主链路,不展开旁支。
- 标记关键对象和关键分支。
- 找测试验证理解。
- 把代码翻译成因果句。
- 写下五行输出。
这个流程的目的不是让阅读变慢,而是让阅读有抓手。
源码不是靠“看得多”读懂的,而是靠“问题足够具体,链路足够闭合,理解能够复述”读懂的。