字段名 | 是否必須 | 允許的值 | 允許的特定字符 |
秒(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)字段的值不區(qū)分大小寫,如: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分鐘開始執(zhí)行一次,之后每隔 15 分鐘執(zhí)行一次(即 3、18、33、48 這些時間點執(zhí)行),這里也可以表示為:3/15
3)逗號(,)
用于枚舉值,如第6個字段值是 MON,WED,FRI,表示 星期一、三、五 執(zhí)行
4)連字號(-)
表示一個范圍,如第3個字段的值為 9-17 表示 9am 到 5pm 直接每個小時(包括9和17)
5)問號(?)
只用于 日(Day of month) 和 星期(Day of week),表示不指定值,可以用于代替 *
6)L,W,#
Go中沒有L,W,#的用法,下文作解釋。
cron舉例說明
每隔5秒執(zhí)行一次:*/5 * * * * ?
每隔1分鐘執(zhí)行一次:0 */1 * * * ?
每天23點執(zhí)行一次:0 0 23 * * ?
每天凌晨1點執(zhí)行一次:0 0 1 * * ?
每月1號凌晨1點執(zhí)行一次:0 0 1 1 * ?
在26分、29分、33分執(zhí)行一次:0 26,29,33 * * * ?
每天的0點、13點、18點、21點都執(zhí)行一次:0 0 0,13,18,21 * * ?
下載安裝
控制臺輸入 go get github.com/robfig/cron
去下載定時任務的Go包,前提是你的 $GOPATH
已經(jīng)配置好
源碼解析
文件目錄講解
constantdelay.go #一個最簡單的秒級別定時系統(tǒng)。與cron無關(guān) constantdelay_test.go #測試 cron.go #Cron系統(tǒng)。管理一系列的cron定時任務(Schedule Job) cron_test.go #測試 doc.go #說明文檔 LICENSE #授權(quán)書 parser.go #解析器,解析cron格式字符串城一個具體的定時器(Schedule) parser_test.go #測試 README.md #README spec.go #單個定時器(Schedule)結(jié)構(gòu)體。如何計算自己的下一次觸發(fā)時間 spec_test.go #測試
cron.go
結(jié)構(gòu)體:
// 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保持任意數(shù)量的條目的軌道,調(diào)用相關(guān)的func時間表指定。它可以被啟動,停止和條目,可運行的同時進行檢查。 type Cron struct { entries []*Entry // 任務 stop chan struct{} // 叫停止的途徑 add chan *Entry // 添加新任務的方式 snapshot chan []*Entry // 請求獲取任務快照的方式 running bool // 是否在運行 ErrorLog *log.Logger // 出錯日志(新增屬性) location *time.Location // 所在地區(qū)(新增屬性) }
// Entry consists of a schedule and the func to execute on that schedule. // 入口包括時間表和可在時間表上執(zhí)行的func type Entry struct { // 計時器 Schedule Schedule // 下次執(zhí)行時間 Next time.Time // 上次執(zhí)行時間 Prev time.Time // 任務 Job Job }
關(guān)鍵方法:
// 開始任務 // 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() } // 結(jié)束任務 // 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 } // 執(zhí)行定時任務 // 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) } // 無限循環(huán) for { //通過對下一個執(zhí)行時間進行排序,判斷那些任務是下一次被執(zhí)行的,防在隊列的前面.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: // 執(zhí)行當前任務 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
結(jié)構(gòu)體及關(guān)鍵方法:
// 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). // 定義了表達式的結(jié)構(gòu)體 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表達式是用來表示一系列時間的,而時間是無法逃脫自己的區(qū)間的 , 分,秒 0 - 59 , 時 0 - 23 , 天/月 0 - 31 , 天/周 0 - 6 , 月0 - 11 。 這些本質(zhì)上都是一個點集合,或者說是一個整數(shù)區(qū)間。 那么對于任意的整數(shù)區(qū)間 , 可以描述cron的如下部分規(guī)則。
至此, robfig/cron為什么不支持 L | W的原因已經(jīng)明了了。去除這兩條規(guī)則后, 其余的規(guī)則其實完全可以使用點的窮舉來通用表示。 考慮到最大的區(qū)間也不過是60個點,那么使用一個uint64的整數(shù)的每一位來表示一個點便很合適了。所以定義unit64不為過
下面是go中cron表達式的方法:
/* ------------------------------------------------------------ 第64位標記任意 , 用于 日/周 , 日 / 月 的相互干擾。 63 - 0 為 表示區(qū)間 [63 , 0] 的 每一個點。 ------------------------------------------------------------ 假設(shè)區(qū)間是 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 optionsDowOptional > 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.optionsDescriptor > 0 { return parseDescriptor(spec) } // Figure out how many fields we need max := 0 for _, place := range places { if p.optionsplace > 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 optionsplace > 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 這里面就不一一贅述了。希望大家能夠自己探索。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。