Go每日一库之48:cron
简介
cron一个用于管理定时任务的库,用 Go 实现 Linux 中crontab这个命令的效果。之前我们也介绍过一个类似的 Go 库——gron。gron代码小巧,用于学习是比较好的。但是它功能相对简单些,并且已经不维护了。如果有定时任务需求,还是建议使用cron。
快速使用
文本代码使用 Go Modules。
创建目录并初始化:
$ mkdir cron && cd cron
$ go mod init github.com/go-quiz/go-daily-lib/cron
安装cron,目前最新稳定版本为 v3:
$ go get -u github.com/robfig/cron/v3
使用:
package main
import (
"fmt"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
c.AddFunc("@every 1s", func() {
fmt.Println("tick every 1 second")
})
c.Start()
time.Sleep(time.Second * 5)
}
使用非常简单,创建cron对象,这个对象用于管理定时任务。
调用cron对象的AddFunc()方法向管理器中添加定时任务。AddFunc()接受两个参数,参数 1 以字符串形式指定触发时间规则,参数 2 是一个无参的函数,每次触发时调用。@every 1s表示每秒触发一次,@every后加一个时间间隔,表示每隔多长时间触发一次。例如@every 1h表示每小时触发一次,@every 1m2s表示每隔 1 分 2 秒触发一次。time.ParseDuration()支持的格式都可以用在这里。
调用c.Start()启动定时循环。
注意一点,因为c.Start()启动一个新的 goroutine 做循环检测,我们在代码最后加了一行time.Sleep(time.Second * 5)防止主 goroutine 退出。
运行效果,每隔 1s 输出一行字符串:
$ go run main.go
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
时间格式
与Linux 中crontab命令相似,cron库支持用 5 个空格分隔的域来表示时间。这 5 个域含义依次为:
Minutes:分钟,取值范围[0-59],支持特殊字符* / , -;Hours:小时,取值范围[0-23],支持特殊字符* / , -;Day of month:每月的第几天,取值范围[1-31],支持特殊字符* / , - ?;Month:月,取值范围[1-12]或者使用月份名字缩写[JAN-DEC],支持特殊字符* / , -;Day of week:周历,取值范围[0-6]或名字缩写[JUN-SAT],支持特殊字符* / , - ?。
注意,月份和周历名称都是不区分大小写的,也就是说SUN/Sun/sun表示同样的含义(都是周日)。
特殊字符含义如下:
*:使用*的域可以匹配任何值,例如将月份域(第 4 个)设置为*,表示每个月;/:用来指定范围的步长,例如将小时域(第 2 个)设置为3-59/15表示第 3 分钟触发,以后每隔 15 分钟触发一次,因此第 2 次触发为第 18 分钟,第 3 次为 33 分钟。。。直到分钟大于 59;,:用来列举一些离散的值和多个范围,例如将周历的域(第 5 个)设置为MON,WED,FRI表示周一、三和五;-:用来表示范围,例如将小时的域(第 1 个)设置为9-17表示上午 9 点到下午 17 点(包括 9 和 17);?:只能用在月历和周历的域中,用来代替*,表示每月/周的任意一天。
了解规则之后,我们可以定义任意时间:
30 * * * *:分钟域为 30,其他域都是*表示任意。每小时的 30 分触发;30 3-6,20-23 * * *:分钟域为 30,小时域的3-6,20-23表示 3 点到 6 点和 20 点到 23 点。3,4,5,6,20,21,22,23 时的 30 分触发;0 0 1 1 *:1(第 4 个) 月 1(第 3 个) 号的 0(第 2 个) 时 0(第 1 个) 分触发。
记熟了这几个域的顺序,再多练习几次很容易就能掌握格式。熟悉规则了之后,就能熟练使用crontab命令了。
func main() {
c := cron.New()
c.AddFunc("30 * * * *", func() {
fmt.Println("Every hour on the half hour")
})
c.AddFunc("30 3-6,20-23 * * *", func() {
fmt.Println("On the half hour of 3-6am, 8-11pm")
})
c.AddFunc("0 0 1 1 *", func() {
fmt.Println("Jun 1 every year")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
预定义时间规则
为了方便使用,cron预定义了一些时间规则:
@yearly:也可以写作@annually,表示每年第一天的 0 点。等价于0 0 1 1 *;@monthly:表示每月第一天的 0 点。等价于0 0 1 * *;@weekly:表示每周第一天的 0 点,注意第一天为周日,即周六结束,周日开始的那个 0 点。等价于0 0 * * 0;@daily:也可以写作@midnight,表示每天 0 点。等价于0 0 * * *;@hourly:表示每小时的开始。等价于0 * * * *。
例如:
func main() {
c := cron.New()
c.AddFunc("@hourly", func() {
fmt.Println("Every hour")
})
c.AddFunc("@daily", func() {
fmt.Println("Every day on midnight")
})
c.AddFunc("@weekly", func() {
fmt.Println("Every week")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
上面代码只是演示用法,实际运行可能要等待非常长的时间才能有输出。
固定时间间隔
cron支持固定时间间隔,格式为:
@every
含义为每隔duration触发一次。会调用time.ParseDuration()函数解析,所以ParseDuration支持的格式都可以。例如1h30m10s。在快速开始部分,我们已经演示了@every的用法了,这里就不赘述了。
时区
默认情况下,所有时间都是基于当前时区的。当然我们也可以指定时区,有 2 两种方式:
- 在时间字符串前面添加一个
CRON_TZ=+ 具体时区,具体时区的格式在之前carbon的文章中有详细介绍。东京时区为Asia/Tokyo,纽约时区为America/New_York; - 创建
cron对象时增加一个时区选项cron.WithLocation(location),location为time.LoadLocation(zone)加载的时区对象,zone为具体的时区格式。或者调用已创建好的cron对象的SetLocation()方法设置时区。
示例:
func main() {
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", func() {
fmt.Println("Every 6 o'clock at New York")
})
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", func() {
fmt.Println("Every 6 o'clock at Tokyo")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
Job接口
除了直接将无参函数作为回调外,cron还支持Job接口:
// cron.go
type Job interface {
Run()
}
我们定义一个实现接口Job的结构:
type GreetingJob struct {
Name string
}
func (g GreetingJob) Run() {
fmt.Println("Hello ", g.Name)
}
调用cron对象的AddJob()方法将GreetingJob对象添加到定时管理器中:
func main() {
c := cron.New()
c.AddJob("@every 1s", GreetingJob{"dj"})
c.Start()
time.Sleep(5 * time.Second)
}
运行效果:
$ go run main.go
Hello dj
Hello dj
Hello dj
Hello dj
Hello dj
使用自定义的结构可以让任务携带状态(Name字段)。
实际上AddFunc()方法内部也调用了AddJob()方法。首先,cron基于func()类型定义一个新的类型FuncJob:
// cron.go
type FuncJob func()
然后让FuncJob实现Job接口:
// cron.go
func (f FuncJob) Run() {
f()
}
在AddFunc()方法中,将传入的回调转为FuncJob类型,然后调用AddJob()方法:
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
return c.AddJob(spec, FuncJob(cmd))
}
线程安全
cron会创建一个新的 goroutine 来执行触发回调。如果这些回调需要并发访问一些资源、数据,我们需要显式地做同步。
自定义时间格式
cron支持灵活的时间格式,如果默认的格式不能满足要求,我们可以自己定义时间格式。时间规则字符串需要cron.Parser对象来解析。我们先来看看默认的解析器是如何工作的。
首先定义各个域:
// parser.go
const (
Second ParseOption = 1