接下来我们先来看看解析器,先挑软柿子捏,哈哈哈。
还从昨天修改Parser解析器开始讲起:
func TestWithParser(t *testing.T) {var parser = NewParser(Dow) // 这里就new了一个解析器c := New(WithParser(parser)) // 这里用新的解析器替换了缺省的解析器if c.parser != parser {t.Error("expected provided parser")}
}
接下来我们分析一下NewParser()函数
// NewParser creates a Parser with custom options.
// NewParser 创建一个 Parser with 自定义选项
//
// It panics if more than one Optional is given, since it would be impossible to
// correctly infer which optional is provided or missing in general.
// 如果给出了多个可选项,则会出现恐慌,因为一般情况下不可能正确地推断哪些可选选项被提供或缺失
//
// Examples
//
// // 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
// 和上面一样,只是不包括时间字段
// specParser := NewParser(Dom | Month | Dow)
// sched, err := specParser.Parse("15 */3 *")
//
// // Same as above, just makes Dow optional
// 和上面一样,只是包括了 Dow optional
// specParser := NewParser(Dom | Month | DowOptional)
// sched, err := specParser.Parse("15 */3")
//
func NewParser(options ParseOption) Parser {optionals := 0if options&DowOptional > 0 {optionals++}if options&SecondOptional > 0 {optionals++}if optionals > 1 { // 这个限制只能是 DowOptional 或者是 SecondOptional 或者没有panic("multiple optionals may not be configured")}return Parser{options} // 生成一个解析类对象
}
我们先来分析这个类型:
其实这个类型是int型,我们看看源码:
// 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 // 其实是int类型// 取值,就是每个宏占一个位,实际传参:
// NewParser(Minute | Hour | Dom | Month | Dow)
const (Second ParseOption = 1 << iota // Seconds field, default 0SecondOptional // Optional seconds field, default 0Minute // Minutes field, default 0Hour // Hours field, default 0Dom // 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.
)
代码最后一句话是生成一个Parser类的对象:
// A custom Parser that can be configured.
type Parser struct {options ParseOption // 成员就只有,选项,其他的就是成员函数了
}
// Parse returns a new crontab schedule representing the given spec.
// Parse返回一个新的crontab计划,表示给定的规范
// It returns a descriptive error if the spec is not valid.
// 如果规范无效,它将返回一个描述性错误。
// It accepts crontab specs and features configured by NewParser.
// 它接受NewParser配置的crontab规范和特性。
func (p Parser) Parse(spec string) (Schedule, error) {if len(spec) == 0 {return nil, fmt.Errorf("empty spec string")}// Extract timezone if presentvar loc = time.Local// TZ是世界标准时间// 例子:TZ=Asia/Tokyoif strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {var err errori := strings.Index(spec, " ")eq := strings.Index(spec, "=")// 转为对应的时间if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)}spec = strings.TrimSpace(spec[i:])}// Handle named schedules (descriptors), if configured// 处理已配置的命名计划(描述符)if strings.HasPrefix(spec, "@") {if p.options&Descriptor == 0 { // 解析器需要支持描述return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)}return parseDescriptor(spec, loc)}// Split on whitespace.fields := strings.Fields(spec) // 对字符串以空格分割 例:5 * * * *// Validate & fill in any omitted or optional fields// 验证并填写任何省略或可选字段var err errorfields, err = normalizeFields(fields, p.options) // 验证对应的参数,并返回真正的参数,如果参数不全,会自动补全if err != nil {return nil, err}field := func(field string, r bounds) uint64 {if err != nil {return 0}var bits uint64bits, 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}return &SpecSchedule{ // 构造一个调度器的对象Second: second,Minute: minute,Hour: hour,Dom: dayofmonth,Month: month,Dow: dayofweek,Location: loc,}, nil
}
这个函数是完整的解析函数,里面包含了TZ世界标准时间和@前缀的描述进行解析。
最后还调用了normalizeFields()这个函数来解析参数,最后的最后生成了一个调度器的对象。
解析@前缀的时间,我们来看看这么解析的:
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
// parseDescriptor返回表达式的预定义调度,如果没有匹配则会出错
func parseDescriptor(descriptor string, loc *time.Location) (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), // 这个要好好分析一下下Location: loc,}, nilcase "@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),Location: loc,}, nilcase "@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,Location: loc,}, nilcase "@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),Location: loc,}, nilcase "@hourly": // 每小时return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: all(hours),Dom: all(dom),Month: all(months),Dow: all(dow),Location: loc,}, nil}// {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},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)
}
all函数这么多,我们来看看里面是啥意思。
// all returns all bits within the given bounds. (plus the star bit)
// All返回给定边界内的所有位。(加上星号位)
func all(r bounds) uint64 { // 这个初看还是有点懵逼的,这个bounds其实也是一个结构体,描述秒,分,时的范围的,我们可以来看看 return getBits(r.min, r.max, 1) | starBit // startBit是一个常量,目前也不知道有啥用
}// starBit
const (// Set the top bit if a star was included in the expression.// 如果表达式中包含星号,则设置顶部位。starBit = 1 << 63
)
// bounds provides a range of acceptable values (plus a map of name to value).
// Bounds提供可接受值的范围(加上名称到值的映射)。
type bounds struct { // 这个一看也是懵逼状态,不知道这个结构的意义,下面我们来看看用这个结构体定义的变量就明白了min, max uintnames 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,}}
)
看到这里就好奇了,这么数据怎么表示我们定时的时间呢?我们继续分析
// getBits sets all bits in the range [min, max], modulo the given step size.
// getBits设置范围[min, max]内的所有位,取给定步长的模。
func getBits(min, max, step uint) uint64 {var bits uint64// If step is 1, use shifts.// 如果step为1,则使用移位。// 我们现在传参是1if 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
}
这一个流程分析完了,我们还留下了疑问,SpecSchedule类的分析,getBits()返回的这些位是有什么用?还有Every()函数返回ConstantDelaySchedule类对象?
这个我们下节带着这些问题好好分析,接下来,我们看看传统配置的处理方式。
这个函数主要是判断传参是否正确,并且把参数解析成为代码默认的参数(因为有些配置只需要年月日,所以把时分秒补上)
// normalizeFields takes a subset set of the time fields and returns the full set
// with defaults (zeroes) populated for unset fields.
// normalizeFields接受时间字段的子集,并返回完整的时间字段集,为未设置的字段填充默认值(零)。
//
// As part of performing this function, it also validates that the provided
// 作为执行这个功能的一部分,它还验证所提供的字段是否与配置的选项兼容。
// fields are compatible with the configured options.
func normalizeFields(fields []string, options ParseOption) ([]string, error) {// Validate optionals & add their field to optionsoptionals := 0if options&SecondOptional > 0 { // 不能很明白SecondOptional 和 Second的区别options |= Secondoptionals++}if options&DowOptional > 0 {options |= Dowoptionals++}// 这里再次判断两个不能一起出现if optionals > 1 {return nil, fmt.Errorf("multiple optionals may not be configured")}// Figure out how many fields we need// 算出我们需要多少字段max := 0for _, place := range places { // place 是自定义的值if options&place > 0 {max++ // 计算最大的字段}}min := max - optionals// Validate number of fields// 验证fields是否正确if count := len(fields); count < min || count > max {if min == max {return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)}return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)}// Populate the optional field if not provided// 如果没有提供,填充可选字段。正常填充是不会进来if min < max && len(fields) == min {switch {case options&DowOptional > 0:fields = append(fields, defaults[5]) // TODO: improve access to defaultcase options&SecondOptional > 0:fields = append([]string{defaults[0]}, fields...)default:return nil, fmt.Errorf("unknown optional field")}}// Populate all fields not part of options with their defaults// 用默认值填充所有不属于选项的字段n := 0expandedFields := make([]string, len(places))copy(expandedFields, defaults) //先以缺省的字段填充for i, place := range places {if options&place > 0 { // 如果没有这个字段,expandedFields就是默认值expandedFields[i] = fields[n] // 填充匹配的字段的值n++}}return expandedFields, nil
}// places就是平常的6个
var places = []ParseOption{Second,Minute,Hour,Dom,Month,Dow,
}// defaults对应默认值
var defaults = []string{"0","0","0","*","*","*",
}
我们接着来分析这个匿名函数,
field := func(field string, r bounds) uint64 {if err != nil {return 0}var bits uint64bits, 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))
func getField(field string, r bounds) (uint64, error) {var bits uint64ranges := 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:
// getRange返回由给定表达式指定的位:
// number | number "-" number [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {var (start, end, step uint// */2 需要解析分号情况rangeAndStep = strings.Split(expr, "/")// 5-6/2 需要直接-lowAndHigh = strings.Split(rangeAndStep[0], "-")singleDigit = len(lowAndHigh) == 1err error)var extra uint64if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { // 如果是*或者? 范围就是最大start = r.minend = r.maxextra = 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) // 如果是 5-6这种, 范围就是 5 到 6if err != nil {return 0, err}default:return 0, fmt.Errorf("too many hyphens: %s", expr)}}switch len(rangeAndStep) { // 看看有没有 5/6 这种情况case 1:step = 1case 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}if step > 1 {extra = 0}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.
// 解析字符串for name,如果有names这个字段,就直接在map中取数据
func parseIntOrName(expr string, names map[string]uint) (uint, error) {if names != nil {if namedInt, ok := names[strings.ToLower(expr)]; ok { // 如果是有names的,就直接在map,取值return namedInt, nil}}return mustParseInt(expr) // 剩下的就只有秒 分 时
}// mustParseInt parses the given expression as an int or returns an error.
// 这个就直接字符串转int了
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
}
其实源码中默认定义了一个标准解析器,如果我们没有传新的解析器,系统就会默认使用这个。
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 requires 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"
func ParseStandard(standardSpec string) (Schedule, error) {return standardParser.Parse(standardSpec)
}
默认解析器是没有秒的,这也是linux的cron的定时,也是没有秒的,所以我们如果需要秒的定时,就需要自定义解析器。这个看上一节就明白了。
这一节主要介绍了解析器,可以自定义解析器。
带着这两个问题,期待下一节分析。
上一篇:VR购物丨抢占电商流量新阵地
下一篇:分支和循环