# 3.11 Cron定时任务

项目地址：<https://github.com/EDDYCJY/go-gin-example>

## 知识点

* 完成定时任务的功能

## 本文目标

在实际的应用项目中，定时任务的使用是很常见的。你是否有过 Golang 如何做定时任务的疑问，莫非是轮询，在本文中我们将结合我们的项目讲述 Cron。

## 介绍

我们将使用 [cron](https://github.com/robfig/cron) 这个包，它实现了 cron 规范解析器和任务运行器，简单来讲就是包含了定时任务所需的功能

### Cron 表达式格式

| 字段名                   | 是否必填 | 允许的值            | 允许的特殊字符    |
| --------------------- | ---- | --------------- | ---------- |
| 秒（Seconds）            | Yes  | 0-59            | \* / , -   |
| 分（Minutes）            | Yes  | 0-59            | \* / , -   |
| 时（Hours）              | Yes  | 0-23            | \* / , -   |
| 一个月中的某天（Day of month） | Yes  | 1-31            | \* / , - ? |
| 月（Month）              | Yes  | 1-12 or JAN-DEC | \* / , -   |
| 星期几（Day of week）      | Yes  | 0-6 or SUN-SAT  | \* / , - ? |

Cron表达式表示一组时间，使用 6 个空格分隔的字段

可以留意到 Golang 的 Cron 比 Crontab 多了一个秒级，以后遇到秒级要求的时候就省事了

### Cron 特殊字符

1、星号 ( \* )

星号表示将匹配字段的所有值

2、斜线 ( / )

斜线用户 描述范围的增量，表现为 “N-MAX/x”，first-last/x 的形式，例如 3-59/15 表示此时的第三分钟和此后的每 15 分钟，到59分钟为止。即从 N 开始，使用增量直到该特定范围结束。它不会重复

3、逗号 ( , )

逗号用于分隔列表中的项目。例如，在 Day of week 使用“MON，WED，FRI”将意味着星期一，星期三和星期五

4、连字符 ( - )

连字符用于定义范围。例如，9 - 17 表示从上午 9 点到下午 5 点的每个小时

5、问号 ( ? )

不指定值，用于代替 “ \* ”，类似 “ \_ ” 的存在，不难理解

### 预定义的 Cron 时间表

| 输入                     | 简述                  | 相当于          |
| ---------------------- | ------------------- | ------------ |
| @yearly (or @annually) | 1月1日午夜运行一次          | 0 0 0 1 1 \* |
| @monthly               | 每个月的午夜，每个月的第一个月运行一次 | 0 0 0 1      |
| @weekly                | 每周一次，周日午夜运行一次       | 0 0 0   0    |
| @daily (or @midnight)  | 每天午夜运行一次            | 0 0 0   \*   |
| @hourly                | 每小时运行一次             | 0 0          |

## 安装

```
$ go get -u github.com/robfig/cron
```

## 实践

在上一章节 [Gin实践 连载十 定制 GORM Callbacks](https://segmentfault.com/a/1190000014393602) 中，我们使用了 GORM 的回调实现了软删除，同时也引入了另外一个问题

就是我怎么硬删除，我什么时候硬删除？这个往往与业务场景有关系，大致为

* 另外有一套硬删除接口
* 定时任务清理（或转移、backup）无效数据

在这里我们选用第二种解决方案来进行实践

### 编写硬删除代码

打开 models 目录下的 tag.go、article.go文件，分别添加以下代码

1、tag.go

```
func CleanAllTag() bool {
    db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Tag{})

    return true
}
```

2、article.go

```
func CleanAllArticle() bool {
    db.Unscoped().Where("deleted_on != ? ", 0).Delete(&Article{})

    return true
}
```

注意硬删除要使用 `Unscoped()`，这是 GORM 的约定

### 编写Cron

在 项目根目录下新建 cron.go 文件，用于编写定时任务的代码，写入文件内容

```
package main

import (
    "time"
    "log"

    "github.com/robfig/cron"

    "github.com/EDDYCJY/go-gin-example/models"
)

func main() {
    log.Println("Starting...")

    c := cron.New()
    c.AddFunc("* * * * * *", func() {
        log.Println("Run models.CleanAllTag...")
        models.CleanAllTag()
    })
    c.AddFunc("* * * * * *", func() {
        log.Println("Run models.CleanAllArticle...")
        models.CleanAllArticle()
    })

    c.Start()

    t1 := time.NewTimer(time.Second * 10)
    for {
        select {
        case <-t1.C:
            t1.Reset(time.Second * 10)
        }
    }
}
```

在这段程序中，我们做了如下的事情

#### cron.New()

会根据本地时间创建一个新（空白）的 Cron job runner

```
func New() *Cron {
    return NewWithLocation(time.Now().Location())
}

// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
    return &Cron{
        entries:  nil,
        add:      make(chan *Entry),
        stop:     make(chan struct{}),
        snapshot: make(chan []*Entry),
        running:  false,
        ErrorLog: nil,
        location: location,
    }
}
```

#### c.AddFunc()

AddFunc 会向 Cron job runner 添加一个 func ，以按给定的时间表运行

```
func (c *Cron) AddJob(spec string, cmd Job) error {
    schedule, err := Parse(spec)
    if err != nil {
        return err
    }
    c.Schedule(schedule, cmd)
    return nil
}
```

会首先解析时间表，如果填写有问题会直接 err，无误则将 func 添加到 Schedule 队列中等待执行

```
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
    entry := &Entry{
        Schedule: schedule,
        Job:      cmd,
    }
    if !c.running {
        c.entries = append(c.entries, entry)
        return
    }

    c.add <- entry
}
```

3、c.Start()

在当前执行的程序中启动 Cron 调度程序。其实这里的主体是 goroutine + for + select + timer 的调度控制哦

```
func (c *Cron) Run() {
    if c.running {
        return
    }
    c.running = true
    c.run()
}
```

#### time.NewTimer + for + select + t1.Reset

如果你是初学者，大概会有疑问，这是干嘛用的？

**（1）time.NewTimer**&#x20;

会创建一个新的定时器，持续你设定的时间 d 后发送一个 channel 消息

**（2）for + select**

阻塞 select 等待 channel

**（3）t1.Reset**

会重置定时器，让它重新开始计时

注：本文适用于 “t.C已经取走，可直接使用 Reset”。

总的来说，这段程序是为了阻塞主程序而编写的，希望你带着疑问来想，有没有别的办法呢？

有的，你直接 `select{}` 也可以完成这个需求 :)

## 验证

```
$ go run cron.go 
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:56
2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:57
2018/04/29 17:03:34 [info] replacing callback `gorm:delete` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:58
2018/04/29 17:03:34 Starting...
2018/04/29 17:03:35 Run models.CleanAllArticle...
2018/04/29 17:03:35 Run models.CleanAllTag...
2018/04/29 17:03:36 Run models.CleanAllArticle...
2018/04/29 17:03:36 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllTag...
2018/04/29 17:03:37 Run models.CleanAllArticle...
```

检查输出日志正常，模拟已软删除的数据，定时任务工作OK

## 小结

定时任务很常见，希望你通过本文能够熟知 Golang 怎么实现一个简单的定时任务调度管理

可以不依赖系统的 Crontab 设置，指不定哪一天就用上了呢

## 问题

如果你手动修改计算机的系统时间，是会导致定时任务错乱的，所以一般不要乱来。

## 参考

### 本系列示例代码

* [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

* 第一版：2018年02月16日发布文章
* 第二版：2019年10月02日修改文章

## ？

如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)
