Go 语言项目源码解析:定时任务库 cron

4,660 阅读5分钟

引子

GitHub 上有很多优秀的开源项目,代码都是透明可见的,每个人只要有账号就可以下载来查看。而我们作为软件开发者来说也可以从中学习到很多知识,以及体会如何正确的工程化、单元测试、统一代码风格等,甚至从源码中找到问题,并提出 Pull Request 来贡献开源社区。今天这篇文章将解析 Go 语言开源项目 robfig/cron源码,这个项目不大,知名度较高,注释也比较清楚,很适合新手学习如何阅读和解析源码。

环境准备

首先我们将源码克隆(Fork)为自己的个人仓库,只需要在 GitHub 项目主页点击 Fork 按钮,然后输入项目名称点击确认即可。克隆完毕后,可以下载到本地,或者直接在科隆后的 GitHub 仓库主页上点击 Create codespace on master 来创建 Codespace。Codespace 是 GitHub 推出的基于 Azure 云服务的远程编程功能,现在对个人账号开放了,可以试一下。

Create Codespace

点击后,浏览器中会打开一个新页面,并会出现在线 VS Code 的界面,然后显示该项目的目录、代码以及终端,如下图。

Codespace

由于我们本次的目的是解析源码,我们主要将在这上面展现和阅读代码,并不会执行它。

现在,我们可以开始解析源码了。

入口文件

解析源码的一个比较好的手段是找到入口文件(Entry File),相当于是一本书的引言(Introduction)章节,项目的整体结构通常会在入口文件中体现出来。

Code Structure

我们从项目介绍 README.md 文件中可以看到,这个定时任务库的使用方式是 cron.New(cron.WithSeconds()) 之类的,也就是 cron.New 方法。因此,我们可以猜测这个方法是在 cron.go 中,我们打开它看一看。

快速扫了一遍之后,我们可以发现这个 New 方法在 113 行,如下图。

Method New

仔细看一下,这个方法就是返回了一个 Cron 类的实例指针,中间的 opts ...Option 参数是一种函数式参数(Functional Option)。而实际的代码实现,无非就是构造了一个 Cron 类的实例指针 c,并对其应用了函数参数,然后返回它。

这样,我们可以判断,真正的定时任务核心逻辑就在 Cron 类中。

不过,无论如何,我们可以确定,入口文件就是 cron.go。接下来只需要分析这个文件包含的核心模块、逻辑就可以大概理清楚整个项目的源码了。

核心类

那么我们再来看一下核心类 Cron 的构造,看看是否有什么新东西。

在代码中搜索一下可以定位到 Cron 类在第 13 行。

Cron Struct

Cron 类有很多属性,包括小写单词表示的私有属性 entrieschainparser 等等,我们暂时还不知道它们各自的含义,不过可以从名称猜测一下。另外,我们还可以看到第 10-12 行的注释描述,意思是 Cron 会追踪 entries,并执行被 schedule 定义的函数,它可以开始运行、结束运行,以及 entries 也会在运行过程中被检查。一脸懵逼?是的,这些描述虽然长,但并不能完全解释清楚,我们只有继续阅读更多源码中的细节,才可以了解清楚。

另外,我们还可以在 Cron 类下面发现 3 个接口以及其描述:

  • ScheduleParser:定时任务的解析器,可以解析并返回 Schedule 实例;
  • Job:已提交的定时任务作业
  • Schedule:用于描述作业的运行周期。

其实,这 3 个接口都很重要,我们从它们的所在位置就可以判断出来。

入口方法

在继续探索之前,我们再回忆一下这个定时任务库的使用方法,除了 cron.New 之外,还需要调用 c.Start() 才能正式生效。因此,我们需要仔细看看 Cron 类的 Start 方法。这其实也是核心类的入口方法(Entry Method)。

我们可以在 cron.go 文件中定位到 Start 方法在第 215 行,如下图。

Cron Start

比较有经验的 Go 语言开发工程师应该会注意到,这是一个典型的原子性操作(Atomic Operation)。c.runningMu 是一个 sync.Mutex 实例,可以加锁(Lock);然后 defer c.runningMu.Unlock() 表示函数调用之后会解锁(Unlock),因此保证重复调用该方法的时候不会出现数据竞速(Data Race);if c.running { return } 的方法表示,如果已经开始运行了,就不会再执行,直接返回;c.running 设置运行状态为 true;最后一行比较关键,go c.run() 表示新起了一个协程(Goroutine)来运行 c.run 方法。因此,我们找到了更核心的方法,run。接下来的工作就是继续解析它了。

是不是很像玩 RPG 游戏时不断寻找机关,最终在千辛万苦之下可喜可贺进入下一关?

总结

等一下,就这么完结撒花了?我那啥都准备好了,你就让我看这个?

我们在这里暂时打住的主要原因是不想让这篇文章变得又臭又长。因为源码解析通常是一个需要耐心繁琐枯燥的过程,而这种过程有时会让读者产生抵触情绪。因此,笔者的主要目的是抛砖引玉,将源码解析的一些核心要领用手把手的方式告诉读者,而读者也会根据自己的理解去实际操作,这样学习起来会更快也会更有意思。

现在稍微总结一下这篇文章用到的解析源码技巧:

  1. 找到入口文件
  2. 定位核心类
  3. 解析入口方法

社区

如果您对笔者的文章感兴趣,可以加笔者微信 tikazyq1 并注明 "码之道",笔者会将你拉入 "码之道" 交流群。