Go 学习笔记(77)— Go 第三方库之 cronexpr(解析 crontab 表达式,定时任务)

cronexpr 支持的比 Linux 自身的 crontab 更详细,可以精确到秒级别。

1. 实现方式

cronexpr 表达式从前到后的顺序如下所示:

字段类型        是否为必须字段   允许的值    	     允许的特殊字符
----------     ----------   --------------    --------------------------
Seconds        No           0-59              * / , -
Minutes        Yes          0-59              * / , -
Hours          Yes          0-23              * / , -
Day of month   Yes          1-31              * / , - L W
Month          Yes          1-12 or JAN-DEC   * / , -
Day of week    Yes          0-6 or SUN-SAT    * / , - L #
Year           No           1970–2099         * / , -

我们定义第 0 个字段为 Seconds,第 1 个字段为 Minutes,各个字段的含义如下:

  • * 星号表示 cron表达式与该字段的所有值匹配。例如,在第四个字段(月)中使用星号表示每个月;

  • / 斜线描述在该时间段的间隔。例如,分钟字段中的 3-59/15表示每小时的第三分钟,此后每15分钟。*/...的形式相当于 first-last/... 的形式,即在该字段的最大可能范围内的增量;

  • , 逗号用于分隔列表中的项目。例如,在第 5 个字段(星期)中使用 MON,WED,FRI表示星期一、星期三和星期五;

  • - 连字符定义范围。例如,2000-2010表示公元 2000 年至 2010 年之间的每一年,包括在内,为闭区间;

  • L 代表 “最后”。当在周天字段中使用时,它允许你指定诸如 “最后一个星期五”(5L)这样的结构。在月日字段中,它指定了该月的最后一天;

  • W 月日字段允许使用 W 字符。这个字符用于指定离给定日期最近的工作日(周一至周五)。举个例子,如果你指定 15W作为月日字段的值,其含义是:“离本月 15 日最近的工作日”。所以,如果 15 号是星期六,触发器在 14 号星期五启动。如果 15 号是星期天,触发器在 16 号星期一启动。如果 15 号是星期二,那么它就在 15 号星期二触发。但是,如果你指定 1W 作为月日的值,并且 1 号是星期六,那么触发器在 3 号星期一发射,因为它不会 "跳过 "一个月的天数边界。

    只有当月日是一个单一的日子,而不是一个范围或日期列表时,才能指定 W 字符。W 字符也可以与 L结合使用,如 LW表示 “本月的最后一个工作日”。

  • # 允许在星期字段中使用 #,后面必须有一个 1 到 5 之间的数字。它允许你指定一些结构,如某个月的 “第二个星期五”。

2. 示例说明

Entry       Description                                                             Equivalent to
@annually   Run once a year at midnight in the morning of January 1                 0 0 0 1 1 * *
@yearly     Run once a year at midnight in the morning of January 1                 0 0 0 1 1 * *
@monthly    Run once a month at midnight in the morning of the first of the month   0 0 0 1 * * *
@weekly     Run once a week at midnight in the morning of Sunday                    0 0 0 * * 0 *
@daily      Run once a day at midnight                                              0 0 0 * * * *
@hourly     Run once an hour at the beginning of the hour                           0 0 * * * * *
@reboot     Not supported

3. 细节说明

  • 如果只有六个字段,则会预加一个 0 秒字段,即 * * * * * 2013在内部成为 0 * * * * 2013
  • 如果只有五个字段,则预设 0 秒字段,并附加一个通配符年份字段,即 * * * * Mon内部成为 0 * * * Mon *
  • 星期字段的域是 [0-7],而不是 [0-6],7 是星期天(像0)。这是为了符合http://linux.die.net/man/5/crontab#;
  • 如果提供了一个错误的cron表达式,代码的行为是不确定的;

4. 安装方式

go get github.com/gorhill/cronexpr

5. 重要函数

5.1 cronexpr.Parse

expr, err := cronexpr.Parse("*/5 * * * * * * ") // 如果表达式解析错误将返回一个错误

5.2 cronexpr.MustParse

expr = cronexpr.MustParse("*/5 * * * * * * ")   // 如果表达式解析错误将直接抛出 panic

6. 代码实现

6.1 简单示例

package main

import (
	"fmt"
	"time"

	"github.com/gorhill/cronexpr"
)

func doTask() {
	fmt.Println("I am running, time is: ", time.Now())
}
func main() {

	// 每隔 5 秒执行1次
	expr, err := cronexpr.Parse("*/5 * * * * * * ") // 如果表达式解析错误将返回一个错误
	if err != nil {
		fmt.Println(err)
		return
	}

	nextTime := expr.Next(time.Now())
	fmt.Println(nextTime)

	time.AfterFunc(time.Until(nextTime), doTask)
	time.Sleep(10 * time.Second)

}

6.2 监控任务和执行任务处于不同的协程

package main

import (
	"fmt"
	"time"

	"github.com/gorhill/cronexpr"
)

// 代表一个任务
type CronJob struct {
	expr     *cronexpr.Expression
	nextTime time.Time // expr.Next(now)
}

func main() {
	// 定时任务字典, key: 任务的名字, value 任务对象
	scheduleTable := make(map[string]*CronJob)
	now := time.Now()

	// 定义定时任务以每 5s 执行一次
	// MustParse 如果遇到解析 contab 错误时会直接抛出 panic ,不会像 Parse 一样返回一个错误
	expr := cronexpr.MustParse("*/5 * * * * * *")
	cronJob := &CronJob{
		expr:     expr,
		nextTime: expr.Next(now),
	}
	// 任务注册到调度表
	scheduleTable["job1"] = cronJob

	// 定义定时任务以每 3s 执行一次
	expr = cronexpr.MustParse("*/3 * * * * * *")
	cronJob = &CronJob{
		expr:     expr,
		nextTime: expr.Next(now),
	}
	// 任务注册到调度表
	scheduleTable["job2"] = cronJob

	// 定时检查一下任务调度表
	for {
		now := time.Now()

		for jobName, cronJob := range scheduleTable {
			// 判断是否到期,当前时间等于定时任务的下次执行时间,或者当前时间大于任务的定时时间
			if now.Equal(cronJob.nextTime) || now.After(cronJob.nextTime) {
				// 启动一个协程, 执行这个任务
				go func(jobName string) {
					fmt.Println("执行:", jobName)
				}(jobName)

				// 计算下一次调度时间
				cronJob.nextTime = cronJob.expr.Next(now)
				fmt.Println(jobName, "下次执行时间:", cronJob.nextTime)
			}
		}

		// 等待 1s,减少 CPU 消耗
		t := <-time.NewTimer(1 * time.Second).C
		fmt.Println(t)
	}

}

你可能感兴趣的:(Go,go,crontab)