Go: 定时任务

写在前面

记录 Go 的一些关于定时任务的处理

内容

基础库

for + sleep

func main() {
    for {
        time.Sleep(5 * time.Second)
        fmt.Println("every 5s")
    }
}

Ticker

func main() {
    ticker := time.NewTicker(5 * time.Second)
    for range ticker.C {
        fmt.Println("every 5s")
    }
}

第三方库

cron

前面两个都是基础库里的东西,可以实现一些基本的定时功能,但对于一些稍显复杂的定时任务需求,就需要我们自己去做更多的内容来支持,比如说需求需要在每个月的 1 号,或是每周一之类的来进行任务,就需要我们自己去写代码来判断了。而 cron 这个第三方库,则能很好地支撑我们这类的需求。

安装

go get github.com/robfig/cron/v3@v3.0.0

cron 对于定时的设置有几种写法,都能达到同样的目的。

@表达式

func descriptors() {

    c := cron.New()
    // @every
    // 支持秒级别,会在启动 6s 后执行
    _, errEveryS := c.AddFunc("@every 6s", func() {
        fmt.Println("every 6s")
    })
    if errEveryS != nil {
        fmt.Printf("errEveryS: %v\n", errEveryS)
    }
    // 0.1 min = 6s
    _, errEveryM := c.AddFunc("@every 0.1m", func() {
        fmt.Println("every 0.1m")
    })

    if errEveryM != nil {
        fmt.Printf("errEveryM: %v\n", errEveryM)
    }

    // @yearly @annually 每年执行一次,即 1 月 1 日的时候开始执行
    _, errYearly := c.AddFunc("@yearly", func() {
        fmt.Println("yearly")
    })
    if errYearly != nil {
        fmt.Printf("errYearly: %v\n", errYearly)
    }

    _, errAnnually := c.AddFunc("@annually", func() {
        fmt.Println("annually")
    })
    if errAnnually != nil {
        fmt.Printf("errAnnually: %v\n", errAnnually)
    }

    // @monthly 每个月的第一天的零点执行
    _, errMonthly := c.AddFunc("@monthly", func() {
        fmt.Println("monthly")
    })
    if errMonthly != nil {
        fmt.Printf("errMonthly: %v\n", errMonthly)
    }

    // @weekly 每周的第一天零点执行,这个第一天可能是周六,也可能是周日
    _, errWeekly := c.AddFunc("@weekly", func() {
        fmt.Println("weekly")
    })
    if errWeekly != nil {
        fmt.Printf("errWeekly: %v\n", errWeekly)
    }

    // @hourly 每小时执行一次,启动后一小时执行
    _, errHourly := c.AddFunc("@hourly", func() {
        fmt.Println("hourly")
    })
    if errHourly != nil {
        fmt.Printf("errHourly: %v\n", errHourly)
    }

    // @daily @midnight,每天执行一次,在每天的零点执行
    _, errDaily := c.AddFunc("@daily", func() {
        fmt.Println("daily")
    })
    if errDaily != nil {
        fmt.Printf("errDaily: %v\n", errDaily)
    }

    _, errMidnight := c.AddFunc("@midnight", func() {
        fmt.Println("midnight")
    })
    if errMidnight != nil {
        fmt.Printf("errMidnight: %v\n", errMidnight)
    }

    c.Start()
    defer c.Stop()

    select {}
}

linux cron 表达式

如果我们想要更灵活的一些处理,就需要学习下 Linux 的 cron 表达式,参照如下:

    *    *    *    *    *
    -    -    -    -    -
    |    |    |    |    |
    |    |    |    |    +----- day of week (0 - 7) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
    |    |    |    +---------- month (1 - 12) OR jan,feb,mar,apr ...
    |    |    +--------------- day of month (1 - 31)
    |    +-------------------- hour (0 - 23)
    +------------------------- minute (0 - 59)

只要我们学会了 Linux 的 cron 表达式的使用,也就可以直接用在 cron 库上,我们可以看到 Linux cron 表达式的标准实现里,最小的时间单位是支持到分钟级别,秒级别的话我们可以用上面的方案处理。

func linux() {
    c := cron.New()

    // 每1分钟执行,等同于 */1 * * * * 和 @every 1m
    _, err := c.AddFunc("* * * * *", func() {
        fmt.Println("* * * * *")
    })
    if err != nil {
        fmt.Printf("err: %v\n", err)
    }

    // 每 5 分钟执行,等同于 @every 5m
    // / 表示频率
    _, err1 := c.AddFunc("*/5 * * * *", func() {
        fmt.Println("* * * * *")
    })
    if err1 != nil {
        fmt.Printf("err1: %v\n", err1)
    }

    // 每个小时的 01 分执行
    _, err2 := c.AddFunc("1 * * * *", func() {
        fmt.Println("1 * * * *")
    })
    if err2 != nil {
        fmt.Printf("err2: %v\n", err2)
    }

    // 每个月的第一天的 0 点 0 分运行,相当于 @monthly
    _, err3 := c.AddFunc("0 0 1 * *", func() {
        fmt.Println("0 0 1 * *")
    })
    if err3 != nil {
        fmt.Printf("err3: %v\n", err3)
    }

    // 每周的第一天(周六或周日)的 0 点 0 分运行,相当于 @weekly
    // 像一些发奖需求,在每周一进行发奖,就可以写成 0 0 * * 1
    _, err4 := c.AddFunc("0 0 * * 0", func() {
        fmt.Println("0 0 * * 0")
    })
    if err4 != nil {
        fmt.Printf("err4: %v\n", err4)
    }

    // 每天的 0 点 0 分运行一次,相当于 @daily 和 @midnight
    _, err5 := c.AddFunc("0 0 * * *", func() {
        fmt.Println("0 0 * * *")
    })
    if err5 != nil {
        fmt.Printf("err5: %v\n", err5)
    }

    // 每年运行一次,在 1 月 1 日 0 点 0 分,相当于 @yearly @annually
    _, err6 := c.AddFunc("0 0 1 1 *", func() {
        fmt.Println("0 0 1 1 *")
    })
    if err6 != nil {
        fmt.Printf("err6: %v\n", err6)
    }

    // 每小时运行一次,相当于 @hourly
    _, err7 := c.AddFunc("0 * * * *", func() {
        fmt.Println("0 * * * *")
    })
    if err7 != nil {
        fmt.Printf("err7: %v\n", err7)
    }

    // - 表示范围 , 表示列表
    // 这里的意思是,在每天的 3 点到 6 点和 20 点到 23 点这两个范围内,每个 30 分执行一次
    // 也就是 3 点 30,4 点 30,5 点 30 这样
    _, err8 := c.AddFunc("30 3-6,20-23 * * *", func() {
        fmt.Println("30 3-6,20-23 * * *")
    })
    if err8 != nil {
        fmt.Printf("err8: %v\n", err8)
    }

    c.Start()
    defer c.Stop()

    select {}
}

更进一步的,假如我们在做海外的应用,有时需要根据时区来进行定时任务,那么可以这么写:

func timezone() {
    c := cron.New()

    // 在东京时区,每天的 4 点 30 分运行
    _, err := c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() {
        fmt.Println("CRON_TZ=Asia/Tokyo 30 04 * * *")
    })
    if err != nil {
        fmt.Printf("err: %v\n", err)
    }

    c.Start()
    defer c.Stop()

    // 或者在 cron 初始化的时候指定时区
    tc, _ := time.LoadLocation("Asia/Tokyo")
    c1 := cron.New(cron.WithLocation(tc))
    _, err1 := c1.AddFunc("30 04 * * *", func() {
        fmt.Println("30 04 * * *")
    })
    if err1 != nil {
        fmt.Printf("err1: %v\n", err1)
    }

    c1.Start()
    defer c1.Stop()

    select {}
}

job

在工作中,为了做到封装的效果,我们可以实现 Job 接口,把任务给封装起来:

func job() {
    c := cron.New()

    j1 := MyJob{name: "1"}
    _, errJob1 := c.AddJob("@every 5s", &j1)
    if errJob1 != nil {
        fmt.Printf("errJob1: %v", errJob1)
    }

    j2 := MyJob{name: "2"}
    _, errJob2 := c.AddJob("@every 6s", &j2)
    if errJob2 != nil {
        fmt.Printf("errJob2: %v", errJob2)
    }
    c.Start()
    select {}
}

// MyJob 对定时任务进行包装
type MyJob struct {
    name string
}

// Run 实现 Job 接口里的 Run 方法
func (j *MyJob) Run() {
    fmt.Printf("myjob : %s\n", j.name)
}

schedule

如果我们想动态的调整任务的下一次运行的时间,就可以通过实现 Schedule 接口来处理:

func schedule() {
    c := cron.New()

    // 为了方便,这里用 FuncJob,就不去创建 Job 结构体了 
    c.Schedule(&MySchedule{}, cron.FuncJob(func() {
        fmt.Println("hello")
    }))

    c.Start()

    select {}

}

type MySchedule struct {
}

// Next 实现 Schedule 接口的 Next 方法
// Start 的时候,会触发 Next 方法,我们可以在这个的基础上,返回下一次的运行时间
// 之后每次触发都会调用 Next,我们就可以在这里做文章,动态修改下一次的触发时间
func (s *MySchedule) Next(t time.Time) time.Time {
    fmt.Printf("now time: %v\n", t)
    result := t.Add(5 * time.Second)
    return result
}

参考

crontab执行时间计算

cron github