文章目录

  • ​​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 := 0
if 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 0
SecondOptional // Optional 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.
)

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 present
var loc = time.Local
// TZ是世界标准时间
// 例子:TZ=Asia/Tokyo
if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
var err error
i := 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 error
fields, 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 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
}

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,
}, 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),
Location: loc,
}, 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,
Location: loc,
}, 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),
Location: loc,
}, 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),
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 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,
}}
)

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

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,则使用移位。
// 我们现在传参是1
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
}

这一个流程分析完了,我们还留下了疑问,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 options
optionals := 0
if options&SecondOptional > 0 { // 不能很明白SecondOptional 和 Second的区别
options |= Second
optionals++
}
if options&DowOptional > 0 {
options |= Dow
optionals++
}
// 这里再次判断两个不能一起出现
if optionals > 1 {
return nil, fmt.Errorf("multiple optionals may not be configured")
}

// Figure out how many fields we need
// 算出我们需要多少字段
max := 0
for _, 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 default
case 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 := 0
expandedFields := 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 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)
)

2.5.2 getField()

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
}

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) == 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) // 如果是 5-6这种, 范围就是 5 到 6
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("too many hyphens: %s", expr)
}
}

switch len(rangeAndStep) { // 看看有没有 5/6 这种情况
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
}
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对象,都需要转参,并且参数是一堆按位操作的数,这些数,这一节也暂不分析。

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