最近需要在 golang 中使用定时任务功能,用到了一个 cron 库,当前是 v3 版本,网上挺多都是 v2 的教程,记录一下使用方法。
在旧版本的库中默认的 cron 表达式不是标准格式,第一个位是秒级的定义。
现在 v3 版本直接用标准 cron 表示式即可,主要看 godoc 文档部分
cron 表示式
推荐使用在线工具来看自己写的 cron 对不对,简单的表达式直接写一般问题不大。这里推荐 crontab.guru,可以通过可视化的方式来查看你编写的定时规则。
以下内容摘自 维基百科-Cron
文件格式說明
┌──分鐘(0 - 59)
│ ┌──小時(0 - 23)
│ │ ┌──日(1 - 31)
│ │ │ ┌─月(1 - 12)
│ │ │ │ ┌─星期(0 - 6,表示从周日到周六)
│ │ │ │ │
* * * * * 被執行的命令
注:
在某些系统里,星期日也可以为 7
不很直观的用法:如果日期和星期同时被设定,那么其中的一个条件被满足时,指令便会被执行。请参考下例。
前 5 个域称之分时日月周,可方便个人记忆。
从第六个域起,指明要执行的命令。
安装
现在都是用的 Go module 进行模块的管理,直接在 goland 中使用 alt + 回车即可同步对应的包 “github.com/robfig/cron/v3”
使用 go get 安装方式如下
go get github.com/robfig/cron/v3
创建配置
建议使用标准的 cron 表达式
// 使用默认的配置
c := cron.New()
// 可以配置如果当前任务正在进行,那么跳过
c := cron.New(cron.WithChain(cron.SkipIfStillRunning(logger)))
// 官方也提供了旧版本的秒级的定义,这个注意你需要传入的 cron 表达式不再是标准 cron 表达式
c := cron.New(cron.WithSeconds())
在上面的代码中出现了一个 logger,我使用的是 logrus,在源码中可以看到 cron 需要的 logger 的定义
// Logger is the interface used in this package for logging, so that any backend
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
type Logger interface {
// Info logs routine messages about cron's operation.
Info(msg string, keysAndValues ...interface{})
// Error logs an error condition.
Error(err error, msg string, keysAndValues ...interface{})
}
那么我们定义了一个 Clog 结构体,实现对应的接口就行了
import (
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
)
type CLog struct {
clog *log.Logger
}
func (l *CLog) Info(msg string, keysAndValues ...interface{}) {
l.clog.WithFields(log.Fields{
"data": keysAndValues,
}).Info(msg)
}
func (l *CLog) Error(err error, msg string, keysAndValues ...interface{}) {
l.clog.WithFields(log.Fields{
"msg": msg,
"data": keysAndValues,
}).Warn(msg)
}
添加任务
启动定时任务有两种方法,分别是传入函数和传入任务。
传入函数
我们看到文档中给出的范例,可以看到任务的添加是通过 c.AddFunc() 这个函数来进行的,直接传入一个函数即可,可以看到定义是 func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error)。
# Runs at 6am in time.Local
cron.New().AddFunc("0 6 * * ?", ...)
# Runs at 6am in America/New_York
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", ...)
// AddFunc adds a func to the Cron to be run on the given schedule.
// The spec is parsed using the time zone of this Cron instance as the default.
// An opaque ID is returned that can be used to later remove it.
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
return c.AddJob(spec, FuncJob(cmd))
}
举个例子,如果你传入的任务仅仅就是一个简单函数进行执行,使用 AddFunc() 就行了,同时也可以通过闭包来引用函数外面的变量,下面是一个完整的例子。
package main
import (
"fmt"
"github.com/robfig/cron/v3"
"time"
)
func TestCron() {
c := cron.New()
i := 1
c.AddFunc("*/1 * * * *", func() {
fmt.Println("每分钟执行一次", i)
i++
})
c.Start()
time.Sleep(time.Minute * 5)
}
func main() {
TestCron()
}
/* output
每分钟执行一次 1
每分钟执行一次 2
每分钟执行一次 3
每分钟执行一次 4
每分钟执行一次 5
*/
传入任务
但是如果我们定义的任务里面还需要留存其他信息呢,可以使用 AddJob() 这个函数,追溯一下源码定义。
// AddJob adds a Job to the Cron to be run on the given schedule.
// The spec is parsed using the time zone of this Cron instance as the default.
// An opaque ID is returned that can be used to later remove it.
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {
schedule, err := c.parser.Parse(spec)
if err != nil {
return 0, err
}
return c.Schedule(schedule, cmd), nil
}
// 可以看到需要传入两个参数,`spec` 就是 cron 表达式,Job 类型我们好像还没见过,点进去看
// Job is an interface for submitted cron jobs.
type Job interface {
Run()
}
现在知道我们的定时任务只需要实现 Run() 这个函数就行了,所以我们可以给出自己的 Job 定义
type Job struct {
A int `json:"a"`
B int `json:"b"`
C string `json:"c"`
Shut chan int `json:"shut"`
}
// implement Run() interface to start rsync job
func (this Job) Run() {
this.A++
fmt.Printf("A: %d\n", this.A)
*this.B++
fmt.Printf("B: %d\n", *this.B)
*this.C += "str"
fmt.Printf("C: %s\n", *this.C)
}
代码例子
给出一个完整代码的示例,我封装了一个 StartJob 函数,方便自己的管理,当然在 c.AddJob() 处可添加多个任务,都会 cron 的要求执行
package main
import (
"fmt"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"time"
)
// 定时任务计划
/*
- spec,传入 cron 时间设置
- job,对应执行的任务
*/
func StartJob(spec string, job Job) {
logger := &CLog{clog: log.New()}
logger.clog.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
c := cron.New(cron.WithChain(cron.SkipIfStillRunning(logger)))
c.AddJob(spec, &job)
// 启动执行任务
c.Start()
// 退出时关闭计划任务
defer c.Stop()
// 如果使用 select{} 那么就一直会循环
select {
case <-job.Shut:
return
}
}
func StopJob(shut chan int) {
shut <- 0
}
type CLog struct {
clog *log.Logger
}
func (l *CLog) Info(msg string, keysAndValues ...interface{}) {
l.clog.WithFields(log.Fields{
"data": keysAndValues,
}).Info(msg)
}
func (l *CLog) Error(err error, msg string, keysAndValues ...interface{}) {
l.clog.WithFields(log.Fields{
"msg": msg,
"data": keysAndValues,
}).Warn(msg)
}
type Job struct {
A int `json:"a"`
B int `json:"b"`
C string `json:"c"`
Shut chan int `json:"shut"`
}
// implement Run() interface to start job
func (j *Job) Run() {
j.A++
fmt.Printf("A: %d\n", j.A)
j.B++
fmt.Printf("B: %d\n", j.B)
j.C += "str"
fmt.Printf("C: %s\n", j.C)
}
func main() {
job1 := Job{
A: 0,
B: 1,
C: "",
Shut: make(chan int, 1),
}
// 每分钟执行一次
go StartJob("*/1 * * * *", job1)
time.Sleep(time.Minute * 3)
}
/*
output
A: 1
B: 2
C: str
A: 2
B: 3
C: strstr
A: 3
B: 4
C: strstrstr
*/
总结
这个 cron 库的 v3 版本直接使用标准 cron 表达式即可
启动 cron 任务有传入函数和传入任务两种方法,如果需要管理建议实现自己的 Job 类
参考资料
robfig/cron
godoc-cron
crontab.guru