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

时间:2022-11-12 10:59:44

环境准备

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

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

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

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

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

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

入口文件

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

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

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

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

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

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

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

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

核心类

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

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

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

​Cron​​ 类有很多属性,包括小写单词表示的私有属性 ​​entries​​、​​chain​​、​​parser​​ 等等,我们暂时还不知道它们各自的含义,不过可以从名称猜测一下。另外,我们还可以看到第 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 行,如下图。

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

比较有经验的 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. 解析入口方法