Go定时器cron的使用详解
cron是什么
cron的意思就是:计划任务,说白了就是定时任务。我和系统约个时间,你在几点几分几秒或者每隔几分钟跑一个任务(job),就那么简单。
cron表达式
cron表达式是一个好东西,这个东西不仅java的quartz能用到,go语言中也可以用到。我没有用过linux的cron,但网上说linux也是可以用crontab -e 命令来配置定时任务。go语言和java中都是可以精确到秒的,但是linux中不行。
cron表达式代表一个时间的集合,使用6个空格分隔的字段表示:
字段名 | 是否必须 | 允许的值 | 允许的特定字符 |
秒(seconds) | 是 | 0-59 | * / , - |
分(minute) | 是 | 0-59 | * / , - |
时(hours) | 是 | 0-23 | * / , - |
日(day of month) | 是 | 1-31 | * / , - ? |
月(month) | 是 | 1-12 或 jan-dec | * / , - |
星期(day of week) | 否 | 0-6 或 sum-sat | * / , - ? |
1.月(month)和星期(day of week)字段的值不区分大小写,如:sun、sun 和 sun 是一样的。
2.星期(day of week)字段如果没提供,相当于是 *
# ┌───────────── min (0 - 59) # │ ┌────────────── hour (0 - 23) # │ │ ┌─────────────── day of month (1 - 31) # │ │ │ ┌──────────────── month (1 - 12) # │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are sunday to # │ │ │ │ │ saturday, or use names; 7 is also sunday) # │ │ │ │ │ # │ │ │ │ │ # * * * * * command to execute
cron特定字符说明
1)星号(*)
表示 cron 表达式能匹配该字段的所有值。如在第5个字段使用星号(month),表示每个月
2)斜线(/)
表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15
3)逗号(,)
用于枚举值,如第6个字段值是 mon,wed,fri,表示 星期一、三、五 执行
4)连字号(-)
表示一个范围,如第3个字段的值为 9-17 表示 9am 到 5pm 直接每个小时(包括9和17)
5)问号(?)
只用于 日(day of month) 和 星期(day of week),表示不指定值,可以用于代替 *
6)l,w,#
go中没有l,w,#的用法,下文作解释。
cron举例说明
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
下载安装
控制台输入 go get github.com/robfig/cron
去下载定时任务的go包,前提是你的 $gopath
已经配置好
源码解析
文件目录讲解
constantdelay.go #一个最简单的秒级别定时系统。与cron无关 constantdelay_test.go #测试 cron.go #cron系统。管理一系列的cron定时任务(schedule job) cron_test.go #测试 doc.go #说明文档 license #授权书 parser.go #解析器,解析cron格式字符串城一个具体的定时器(schedule) parser_test.go #测试 readme.md #readme spec.go #单个定时器(schedule)结构体。如何计算自己的下一次触发时间 spec_test.go #测试
cron.go
结构体:
// cron keeps track of any number of entries, invoking the associated func as // specified by the schedule. it may be started, stopped, and the entries may // be inspected while running. // cron保持任意数量的条目的轨道,调用相关的func时间表指定。它可以被启动,停止和条目,可运行的同时进行检查。 type cron struct { entries []*entry // 任务 stop chan struct{} // 叫停止的途径 add chan *entry // 添加新任务的方式 snapshot chan []*entry // 请求获取任务快照的方式 running bool // 是否在运行 errorlog *log.logger // 出错日志(新增属性) location *time.location // 所在地区(新增属性) }
// entry consists of a schedule and the func to execute on that schedule. // 入口包括时间表和可在时间表上执行的func type entry struct { // 计时器 schedule schedule // 下次执行时间 next time.time // 上次执行时间 prev time.time // 任务 job job }
关键方法:
// 开始任务 // start the cron scheduler in its own go-routine, or no-op if already started. func (c *cron) start() { if c.running { return } c.running = true go c.run() } // 结束任务 // stop stops the cron scheduler if it is running; otherwise it does nothing. func (c *cron) stop() { if !c.running { return } c.stop <- struct{}{} c.running = false } // 执行定时任务 // run the scheduler.. this is private just due to the need to synchronize // access to the 'running' state variable. func (c *cron) run() { // figure out the next activation times for each entry. now := time.now().in(c.location) for _, entry := range c.entries { entry.next = entry.schedule.next(now) } // 无限循环 for { //通过对下一个执行时间进行排序,判断那些任务是下一次被执行的,防在队列的前面.sort是用来做排序的 sort.sort(bytime(c.entries)) var effective time.time if len(c.entries) == 0 || c.entries[0].next.iszero() { // if there are no entries yet, just sleep - it still handles new entries // and stop requests. effective = now.adddate(10, 0, 0) } else { effective = c.entries[0].next } timer := time.newtimer(effective.sub(now)) select { case now = <-timer.c: // 执行当前任务 now = now.in(c.location) // run every entry whose next time was this effective time. for _, e := range c.entries { if e.next != effective { break } go c.runwithrecovery(e.job) e.prev = e.next e.next = e.schedule.next(now) } continue case newentry := <-c.add: // 添加新的任务 c.entries = append(c.entries, newentry) newentry.next = newentry.schedule.next(time.now().in(c.location)) case <-c.snapshot: // 获取快照 c.snapshot <- c.entrysnapshot() case <-c.stop: // 停止任务 timer.stop() return } // 'now' should be updated after newentry and snapshot cases. now = time.now().in(c.location) timer.stop() } }
spec.go
结构体及关键方法:
// specschedule specifies a duty cycle (to the second granularity), based on a // traditional crontab specification. it is computed initially and stored as bit sets. type specschedule struct { // 表达式中锁表明的,秒,分,时,日,月,周,每个都是uint64 // dom:day of month,dow:day of week second, minute, hour, dom, month, dow uint64 } // bounds provides a range of acceptable values (plus a map of name to value). // 定义了表达式的结构体 type bounds struct { min, max uint names map[string]uint } // the bounds for each field. // 这样就能看出各个表达式的范围 var ( seconds = bounds{0, 59, nil} minutes = bounds{0, 59, nil} hours = bounds{0, 23, nil} dom = bounds{1, 31, nil} months = bounds{1, 12, map[string]uint{ "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12, }} dow = bounds{0, 6, map[string]uint{ "sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, }} ) const ( // set the top bit if a star was included in the expression. starbit = 1 << 63 )
看了上面的东西肯定有人疑惑为什么秒分时这些都是定义了unit64,以及定义了一个常量starbit = 1 << 63这种写法,这是逻辑运算符。表示二进制1向左移动63位。原因如下:
cron表达式是用来表示一系列时间的,而时间是无法逃脱自己的区间的 , 分,秒 0 - 59 , 时 0 - 23 , 天/月 0 - 31 , 天/周 0 - 6 , 月0 - 11 。 这些本质上都是一个点集合,或者说是一个整数区间。 那么对于任意的整数区间 , 可以描述cron的如下部分规则。
- * | ? 任意 , 对应区间上的所有点。 ( 额外注意 日/周 , 日 / 月 的相互干扰。)
- 纯数字 , 对应一个具体的点。
- / 分割的两个数字 a , b, 区间上符合 a + n * b 的所有点 ( n >= 0 )。
- - 分割的两个数字, 对应这两个数字决定的区间内的所有点。
- l | w 需要对于特定的时间特殊判断, 无法通用的对应到区间上的点。
至此, robfig/cron为什么不支持 l | w的原因已经明了了。去除这两条规则后, 其余的规则其实完全可以使用点的穷举来通用表示。 考虑到最大的区间也不过是60个点,那么使用一个uint64的整数的每一位来表示一个点便很合适了。所以定义unit64不为过
下面是go中cron表达式的方法:
/* ------------------------------------------------------------ 第64位标记任意 , 用于 日/周 , 日 / 月 的相互干扰。 63 - 0 为 表示区间 [63 , 0] 的 每一个点。 ------------------------------------------------------------ 假设区间是 0 - 63 , 则有如下的例子 : 比如 0/3 的表示如下 : (表示每隔两位为1) * / ? +---+--------------------------------------------------------+ | 0 | 1 0 0 1 0 0 1 ~~ ~~ 1 0 0 1 0 0 1 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 比如 2-5 的表示如下 : (表示从右往左2-5位上都是1) * / ? +---+--------------------------------------------------------+ | 0 | 0 0 0 0 ~ ~ ~~ ~ 0 0 0 1 1 1 1 0 0 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 比如 * 的表示如下 : (表示所有位置上都为1) * / ? +---+--------------------------------------------------------+ | 1 | 1 1 1 1 1 ~ ~ ~ 1 1 1 1 1 1 1 1 1 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 */
parser.go
将字符串解析为specschedule的类。
package cron import ( "fmt" "math" "strconv" "strings" "time" ) // configuration options for creating a parser. most options specify which // fields should be included, while others enable features. if a field is not // included the parser will assume a default value. these options do not change // the order fields are parse in. type parseoption int const ( second parseoption = 1 << iota // seconds field, default 0 minute // minutes field, default 0 hour // hours field, default 0 dom // day of month field, default * month // month field, default * dow // day of week field, default * dowoptional // optional day of week field, default * descriptor // allow descriptors such as @monthly, @weekly, etc. ) var places = []parseoption{ second, minute, hour, dom, month, dow, } var defaults = []string{ "0", "0", "0", "*", "*", "*", } // a custom parser that can be configured. type parser struct { options parseoption optionals int } // creates a custom parser with custom options. // // // standard parser without descriptors // specparser := newparser(minute | hour | dom | month | dow) // sched, err := specparser.parse("0 0 15 */3 *") // // // same as above, just excludes time fields // subsparser := newparser(dom | month | dow) // sched, err := specparser.parse("15 */3 *") // // // same as above, just makes dow optional // subsparser := newparser(dom | month | dowoptional) // sched, err := specparser.parse("15 */3") // func newparser(options parseoption) parser { optionals := 0 if options&dowoptional > 0 { options |= dow optionals++ } return parser{options, optionals} } // parse returns a new crontab schedule representing the given spec. // it returns a descriptive error if the spec is not valid. // it accepts crontab specs and features configured by newparser. // 将字符串解析成为specschedule 。 specschedule符合schedule接口 func (p parser) parse(spec string) (schedule, error) { // 直接处理特殊的特殊的字符串 if spec[0] == '@' && p.options&descriptor > 0 { return parsedescriptor(spec) } // figure out how many fields we need max := 0 for _, place := range places { if p.options&place > 0 { max++ } } min := max - p.optionals // cron利用空白拆解出独立的items。 fields := strings.fields(spec) // 验证表达式取值范围 if count := len(fields); count < min || count > max { if min == max { return nil, fmt.errorf("expected exactly %d fields, found %d: %s", min, count, spec) } return nil, fmt.errorf("expected %d to %d fields, found %d: %s", min, max, count, spec) } // fill in missing fields fields = expandfields(fields, p.options) var err error field := func(field string, r bounds) uint64 { if err != nil { return 0 } var bits uint64 bits, err = getfield(field, r) return bits } var ( second = field(fields[0], seconds) minute = field(fields[1], minutes) hour = field(fields[2], hours) dayofmonth = field(fields[3], dom) month = field(fields[4], months) dayofweek = field(fields[5], dow) ) if err != nil { return nil, err } // 返回所需要的specschedule return &specschedule{ second: second, minute: minute, hour: hour, dom: dayofmonth, month: month, dow: dayofweek, }, nil } func expandfields(fields []string, options parseoption) []string { n := 0 count := len(fields) expfields := make([]string, len(places)) copy(expfields, defaults) for i, place := range places { if options&place > 0 { expfields[i] = fields[n] n++ } if n == count { break } } return expfields } var standardparser = newparser( minute | hour | dom | month | dow | descriptor, ) // parsestandard returns a new crontab schedule representing the given standardspec // (https://en.wikipedia.org/wiki/cron). it differs from parse requiring to always // pass 5 entries representing: minute, hour, day of month, month and day of week, // in that order. it returns a descriptive error if the spec is not valid. // // it accepts // - standard crontab specs, e.g. "* * * * ?" // - descriptors, e.g. "@midnight", "@every 1h30m" // 这里表示不仅可以使用cron表达式,也可以使用@midnight @every等方法 func parsestandard(standardspec string) (schedule, error) { return standardparser.parse(standardspec) } var defaultparser = newparser( second | minute | hour | dom | month | dowoptional | descriptor, ) // parse returns a new crontab schedule representing the given spec. // it returns a descriptive error if the spec is not valid. // // it accepts // - full crontab specs, e.g. "* * * * * ?" // - descriptors, e.g. "@midnight", "@every 1h30m" func parse(spec string) (schedule, error) { return defaultparser.parse(spec) } // getfield returns an int with the bits set representing all of the times that // the field represents or error parsing field value. a "field" is a comma-separated // list of "ranges". func getfield(field string, r bounds) (uint64, error) { var bits uint64 ranges := strings.fieldsfunc(field, func(r rune) bool { return r == ',' }) for _, expr := range ranges { bit, err := getrange(expr, r) if err != nil { return bits, err } bits |= bit } return bits, nil } // getrange returns the bits indicated by the given expression: // number | number "-" number [ "/" number ] // or error parsing range. func getrange(expr string, r bounds) (uint64, error) { var ( start, end, step uint rangeandstep = strings.split(expr, "/") lowandhigh = strings.split(rangeandstep[0], "-") singledigit = len(lowandhigh) == 1 err error ) var extra uint64 if lowandhigh[0] == "*" || lowandhigh[0] == "?" { start = r.min end = r.max extra = starbit } else { start, err = parseintorname(lowandhigh[0], r.names) if err != nil { return 0, err } switch len(lowandhigh) { case 1: end = start case 2: end, err = parseintorname(lowandhigh[1], r.names) if err != nil { return 0, err } default: return 0, fmt.errorf("too many hyphens: %s", expr) } } switch len(rangeandstep) { case 1: step = 1 case 2: step, err = mustparseint(rangeandstep[1]) if err != nil { return 0, err } // special handling: "n/step" means "n-max/step". if singledigit { end = r.max } default: return 0, fmt.errorf("too many slashes: %s", expr) } if start < r.min { return 0, fmt.errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr) } if end > r.max { return 0, fmt.errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr) } if start > end { return 0, fmt.errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr) } if step == 0 { return 0, fmt.errorf("step of range should be a positive number: %s", expr) } return getbits(start, end, step) | extra, nil } // parseintorname returns the (possibly-named) integer contained in expr. func parseintorname(expr string, names map[string]uint) (uint, error) { if names != nil { if namedint, ok := names[strings.tolower(expr)]; ok { return namedint, nil } } return mustparseint(expr) } // mustparseint parses the given expression as an int or returns an error. func mustparseint(expr string) (uint, error) { num, err := strconv.atoi(expr) if err != nil { return 0, fmt.errorf("failed to parse int from %s: %s", expr, err) } if num < 0 { return 0, fmt.errorf("negative number (%d) not allowed: %s", num, expr) } return uint(num), nil } // getbits sets all bits in the range [min, max], modulo the given step size. func getbits(min, max, step uint) uint64 { var bits uint64 // if step is 1, use shifts. if step == 1 { return ^(math.maxuint64 << (max + 1)) & (math.maxuint64 << min) } // else, use a simple loop. for i := min; i <= max; i += step { bits |= 1 << i } return bits } // all returns all bits within the given bounds. (plus the star bit) func all(r bounds) uint64 { return getbits(r.min, r.max, 1) | starbit } // parsedescriptor returns a predefined schedule for the expression, or error if none matches. func parsedescriptor(descriptor string) (schedule, error) { switch descriptor { case "@yearly", "@annually": return &specschedule{ second: 1 << seconds.min, minute: 1 << minutes.min, hour: 1 << hours.min, dom: 1 << dom.min, month: 1 << months.min, dow: all(dow), }, nil case "@monthly": return &specschedule{ second: 1 << seconds.min, minute: 1 << minutes.min, hour: 1 << hours.min, dom: 1 << dom.min, month: all(months), dow: all(dow), }, nil case "@weekly": return &specschedule{ second: 1 << seconds.min, minute: 1 << minutes.min, hour: 1 << hours.min, dom: all(dom), month: all(months), dow: 1 << dow.min, }, nil case "@daily", "@midnight": return &specschedule{ second: 1 << seconds.min, minute: 1 << minutes.min, hour: 1 << hours.min, dom: all(dom), month: all(months), dow: all(dow), }, nil case "@hourly": return &specschedule{ second: 1 << seconds.min, minute: 1 << minutes.min, hour: all(hours), dom: all(dom), month: all(months), dow: all(dow), }, nil } const every = "@every " if strings.hasprefix(descriptor, every) { duration, err := time.parseduration(descriptor[len(every):]) if err != nil { return nil, fmt.errorf("failed to parse duration %s: %s", descriptor, err) } return every(duration), nil } return nil, fmt.errorf("unrecognized descriptor: %s", descriptor) }
项目中应用
package main import ( "github.com/robfig/cron" "log" ) func main() { i := 0 c := cron.new() spec := "*/5 * * * * ?" c.addfunc(spec, func() { i++ log.println("cron running:", i) }) c.addfunc("@every 1h1m", func() { i++ log.println("cron running:", i) }) c.start() }
注: @every 用法比较特殊,这是go里面比较特色的用法。同样的还有 @yearly @annually @monthly @weekly @daily @midnight @hourly 这里面就不一一赘述了。希望大家能够自己探索。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。