go-cron源码分析(二、Parser解析器)
创始人
2024-04-05 10:13:24
0

文章目录

    • 2.1 Parser解析器
    • 2.2 NewParser()解析
      • 2.2.1 ParseOption类型
      • 2.2.2 Parser解析类
    • 2.3 func (p Parser) Parse(spec string)(Schedule, error)
    • 2.4 parseDescriptor(descriptor string, loc *time.Location)
      • 2.4.1 all()
      • 2.4.2 bounds
      • 2.4.3 getBits()
    • 2.5 normalizeFields()
      • 2.5.1 field匿名函数
      • 2.5.2 getField()
      • 2.5.3 getRange()
      • 2.5.4 其他工具函数
    • 2.6 standardParser
    • 2.7 总结

2.1 Parser解析器

接下来我们先来看看解析器,先挑软柿子捏,哈哈哈。

还从昨天修改Parser解析器开始讲起:

func TestWithParser(t *testing.T) {var parser = NewParser(Dow)			// 这里就new了一个解析器c := New(WithParser(parser))		// 这里用新的解析器替换了缺省的解析器if c.parser != parser {t.Error("expected provided parser")}
}

2.2 NewParser()解析

接下来我们分析一下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}   // 生成一个解析类对象
}

2.2.1 ParseOption类型

我们先来分析这个类型:

其实这个类型是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.
)

2.2.2 Parser解析类

代码最后一句话是生成一个Parser类的对象:

// A custom Parser that can be configured.
type Parser struct {options ParseOption			// 成员就只有,选项,其他的就是成员函数了
}

2.3 func (p Parser) Parse(spec string)(Schedule, error)

// 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()这个函数来解析参数,最后的最后生成了一个调度器的对象。

2.4 parseDescriptor(descriptor string, loc *time.Location)

解析@前缀的时间,我们来看看这么解析的:

// 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)
}

2.4.1 all()

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
)

2.4.2 bounds

// 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,}}
)

看到这里就好奇了,这么数据怎么表示我们定时的时间呢?我们继续分析

2.4.3 getBits()

// 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类对象?

这个我们下节带着这些问题好好分析,接下来,我们看看传统配置的处理方式。

2.5 normalizeFields()

这个函数主要是判断传参是否正确,并且把参数解析成为代码默认的参数(因为有些配置只需要年月日,所以把时分秒补上)

// 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","*","*","*",
}

2.5.1 field匿名函数

我们接着来分析这个匿名函数,

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))

2.5.2 getField()

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
}

2.5.3 getRange()

分析传参,并且通过参数返回对应的位数据

// 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    // 这个步进在这里用的
}

2.5.4 其他工具函数

// 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
}

2.6 standardParser

其实源码中默认定义了一个标准解析器,如果我们没有传新的解析器,系统就会默认使用这个。

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的定时,也是没有秒的,所以我们如果需要秒的定时,就需要自定义解析器。这个看上一节就明白了。

2.7 总结

这一节主要介绍了解析器,可以自定义解析器。

  1. 系统始终是6位的解析器,如果自定义解析器减少了某一个,系统会自动补成缺省值。
  2. 解析器有两种格式,一种是@xxx 一种是 x x x x x。两种解析走不同分支
  3. 不管走那个分支,最后都是生成一个SpecSchedule对象或者ConstantDelaySchedule对象,这个这一节暂不分析
  4. 生成SpecSchedule对象,都需要转参,并且参数是一堆按位操作的数,这些数,这一节也暂不分析。

带着这两个问题,期待下一节分析。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...