golang cron v3 定時任務

最近需要在 golang 中使用定時任務功能,用到了一個 cron[1] 庫,當前是 v3 版本,網上挺多都是 v2 的教程,記錄一下使用方法。

在舊版本的庫中默認的 cron 表達式不是標準格式,第一個位是秒級的定義。現在 v3 版本直接用標準 cron 表示式即可,主要看 godoc 文檔部分 [2]

cron 表示式

推薦使用在線工具來看自己寫的 cron 對不對,簡單的表達式直接寫一般問題不大。這裏推薦 crontab.guru[3],可以通過可視化的方式來查看你編寫的定時規則。

crontab-guru

以下內容摘自維基百科 - Cron[4]

# 文件格式說明
# ┌──分鐘(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
*/

總結

參考資料

[1] cron: https://github.com/robfig/cron

[2] godoc 文檔部分: https://godoc.org/github.com/robfig/cron

[3] crontab.guru: https://crontab.guru/

[4] 維基百科 - Cron: https://zh.wikipedia.org/wiki/Cron

[5] robfig/cron: https://github.com/robfig/cron

[6] godoc-cron: http://godoc.org/github.com/robfig/cron

[7] crontab.guru: https://crontab.guru/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ag9FIrSWoh49BG5awy642w