• go-cron源码分析(一、由初始化引起的一些其他事)


    最近在实现go的定时功能,所以先来了解一波cron的实现,看看是否可以借鉴。

    1.1 Cron用法

    我们先来看看用法:

    c := cron.New()     // 新建一个定时器任务
    c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })   // 绑定定时任务
    c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
    c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
    c.AddFunc("@hourly",      func() { fmt.Println("Every hour, starting an hour from now") })
    c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
    c.Start()    // 开始调度
    ..
    // Funcs are invoked in their own goroutine, asynchronously.
    ...
    // Funcs may also be added to a running Cron
    c.AddFunc("@daily", func() { fmt.Println("Every day") })
    ..
    // Inspect the cron job entries' next and previous run times.
    inspect(c.Entries())  // 看一下下一个任务和上一个任务
    ..
    c.Stop()  // Stop the scheduler (does not stop any jobs already running).
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1.2 cron.new()

    创建一个cron的对象,没看源码之前,一直觉得new()就可以创建一个对象了,没想到看了源码之后才发现,这个是需要带参的。

    // New returns a new Cron job runner, modified by the given options.
    // New返回一个新的Cron作业运行器,由给定选项修改。
    //
    // Available Settings
    // 可用的设置
    //
    //   Time Zone
    //     Description: The time zone in which schedules are interpreted
    //     描述:解释时间表的时区
    //     Default:     time.Local
    //
    //   Parser
    //     Description: Parser converts cron spec strings into cron.Schedules.
    //     描述:解析器将cron规范字符串转换为cron. schedules。
    //     Default:     Accepts this spec: https://en.wikipedia.org/wiki/Cron
    //
    //   Chain
    //     Description: Wrap submitted jobs to customize behavior.
    //     描述:对提交的作业进行包装以定制行为。
    //     Default:     A chain that recovers panics and logs them to stderr.
    //
    // See "cron.With*" to modify the default behavior.
    func New(opts ...Option) *Cron {
    	c := &Cron{
    		entries:   nil,
    		chain:     NewChain(),
    		add:       make(chan *Entry),
    		stop:      make(chan struct{}),
    		snapshot:  make(chan chan []Entry),
    		remove:    make(chan EntryID),
    		running:   false,
    		runningMu: sync.Mutex{},
    		logger:    DefaultLogger,
    		location:  time.Local,
    		parser:    standardParser,
    	}
    	for _, opt := range opts {
    		opt(c)			// 可变参数都是函数,这样执行
    	}
    	return c
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    这个函数接受可变参数,这个可变参数其实是函数,由下面的opt©来执行。

    1.2.1 time zone

    下面我们先看简单的time zone:

    // 代码路径 option_test.go
    func TestWithLocation(t *testing.T) {	// 其实这是一个测试用例
    	c := New(WithLocation(time.UTC))	
    	if c.location != time.UTC {
    		t.Errorf("expected UTC, got %v", c.location)
    	}
    }
    
    // WithLocation overrides the timezone of the cron instance.
    func WithLocation(loc *time.Location) Option {
    	return func(c *Cron) {    // 这个函数也比较简单,就是返回一个匿名函数,这个匿名函数去修改c的location的值
    		c.location = loc
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.2.2 Parser

    func TestWithParser(t *testing.T) {
    	var parser = NewParser(Dow)			// 主动申请一个解析器,这个比较难,之后在看
    	c := New(WithParser(parser))        // 通过这个函数修改默认的解析器
    	if c.parser != parser {
    		t.Error("expected provided parser")
    	}
    }
    
    // WithParser overrides the parser used for interpreting job schedules.
    func WithParser(p ScheduleParser) Option {		// 这个解析器也比较简单
    	return func(c *Cron) {
    		c.parser = p
    	}
    }
    
    // 不过在这里我看到一个比较有意思的解析器,有带秒的解析器
    // WithSeconds overrides the parser used for interpreting job schedules to
    // include a seconds field as the first one.
    func WithSeconds() Option {						// 我们上面的例子,并没有秒的定时任务,有了这个自定义解析器,我们就可以实现秒定时了
    	return WithParser(NewParser(
    		Second | Minute | Hour | Dom | Month | Dow | Descriptor,
    	))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    实现秒定时:

    import (
    	"fmt"
    	"github.com/robfig/cron/v3"
    	"time"
    )
    
    func main() {
    	fmt.Println("time ", time.Now())
    	c := cron.New(cron.WithSeconds())   // 指定自定义解析器
        // 这种绑定就是1秒执行一次
    	c.AddFunc("*/1 * * * * *", func() { fmt.Println("Every hour on the half hour", time.Now()) })
    	c.Start()
    
    	select {}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.2.3 Chain

    这个chain并没有简单的例子,可以等到分析Chain再详细说明

    // WithChain specifies Job wrappers to apply to all jobs added to this cron.
    // Refer to the Chain* functions in this package for provided wrappers.
    func WithChain(wrappers ...JobWrapper) Option {   // 只是提供了一个简单的修改chain
    	return func(c *Cron) {
    		c.chain = NewChain(wrappers...)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.2.4 logger

    修改日志,这个可以好好分析分析了

    func TestWithVerboseLogger(t *testing.T) {
    	var buf syncWriter
    	var logger = log.New(&buf, "", log.LstdFlags)			// 自己自定义了一个log
    	c := New(WithLogger(VerbosePrintfLogger(logger)))		// 绑定自己指定以的log
    	if c.logger.(printfLogger).logger != logger {
    		t.Error("expected provided logger")
    	}
    
    	c.AddFunc("@every 1s", func() {})
    	c.Start()
    	time.Sleep(OneSecond)
    	c.Stop()
    	out := buf.String()
    	if !strings.Contains(out, "schedule,") ||
    		!strings.Contains(out, "run,") {
    		t.Error("expected to see some actions, got:", out)
    	}
    }
    
    // VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
    // "log") into an implementation of the Logger interface which logs everything.
    func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {  // 需要自己使用Printf函数
    	return printfLogger{l, true}   // 这是返回一个Logger对象
    }
    
    // WithLogger uses the provided logger.
    func WithLogger(logger Logger) Option {
    	return func(c *Cron) {				// 实际调用的也是这样修改
    		c.logger = logger		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    那我们实现一下自己的日志库,能不能打印出来

    package main
    
    // 需要拉取项目:go get github.com/robfig/cron/v3@v3.0.0
    
    import (
    	"fmt"
    	"github.com/robfig/cron/v3"
    	"time"
    )
    
    type MyLog struct {
    	buff string
    }
    
    func (this *MyLog)Printf(format string, v ...interface{}) {  // 看了下日志源码,只要实现这个接口,就可以传参
    	this.buff = fmt.Sprintf("mylog ", time.Now(), "hahahh")
    }
    
    
    func main() {
    	fmt.Println("time ", time.Now())
    	var mylog MyLog
    	// cron.VerbosePrintfLogger(&mylog) 这个就是把自己的日志,转成cron的日志
    	c := cron.New(cron.WithSeconds(), cron.WithLogger(cron.VerbosePrintfLogger(&mylog)))
    	//c.AddFunc("*/1 * * * * *", func() { fmt.Println("Every hour on the half hour", time.Now()) })
    	c.AddFunc("*/1 * * * * *", func() { fmt.Println("log ", mylog.buff) })  // 1秒打一个
    	c.Start()
    
    
    	select {}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    看看输出:

    time  2022-11-03 20:22:31.0890216 +0800 CST m=+0.001725601
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:32.0028412 +0800 CST m=+0.915545201, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:33.0008532 +0800 CST m=+1.913557201, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:34.0109866 +0800 CST m=+2.923690601, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:35.0028254 +0800 CST m=+3.915529401, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:36.0023282 +0800 CST m=+4.915032201, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:37.0020753 +0800 CST m=+5.914779301, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:38.0028339 +0800 CST m=+6.915537901, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:39.0023824 +0800 CST m=+7.915086401, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:40.0028723 +0800 CST m=+8.915576301, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:41.0037012 +0800 CST m=+9.916405201, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:42.0035357 +0800 CST m=+10.916239701, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:43.0024844 +0800 CST m=+11.915188401, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:44.0039952 +0800 CST m=+12.916699201, string=hahahh)
    log  mylog %!(EXTRA time.Time=2022-11-03 20:22:45.002594 +0800 CST m=+13.915298001, string=hahahh)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    虽然日志没有什么实际用处,但可以熟悉一个cron的日志使用。

    1.3 Logger.go

    看着字数很少,就跟着分析一下日志吧,上面说过只要实现Printf函数即可,那我们就来看看。

    1.3.1 DefaultLogger

    首先来看看缺省日志

    // DefaultLogger is used by Cron if none is specified.
    var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
    
    // 分析PrintfLogger这个函数
    // PrintfLogger wraps a Printf-based logger (such as the standard library "log")
    // into an implementation of the Logger interface which logs errors only.
    // PrintfLogger将基于printf的记录器(如标准库“日志”)封装到logger接口的实现中,该实现只记录错误。
    // 这个函数接受一个Printf函数作为接口l,我们上面的log对象中就有Printf函数,可以追踪进去看看
    func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
    	return printfLogger{l, false}		// 这是一个结构体,返回结构题的对象
    }
    
    // 这里还差了一个Logger对象是啥?
    // Logger is the interface used in this package for logging, so that any backend
    // can be plugged in. It is a subset of the github.com/go-logr/logr interface.
    type Logger interface {
    	// Info logs routine messages about cron's operation.
    	Info(msg string, keysAndValues ...interface{})
    	// Error logs an error condition.
    	Error(err error, msg string, keysAndValues ...interface{})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    看到这里就明白了,原来搞了这么半天,原来是为了实现Logger接口,是通过那两个函数来实现的

    1.3.2 printfLogger结构体

    // 通过分析Logger接口,这个必然会出Info Error两个函数,才能显示Logger接口
    type printfLogger struct {
    	logger  interface{ Printf(string, ...interface{}) }
    	logInfo bool
    }
    
    func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) {	 // 果然有
    	if pl.logInfo {
    		keysAndValues = formatTimes(keysAndValues)
    		pl.logger.Printf(
    			formatString(len(keysAndValues)),
    			append([]interface{}{msg}, keysAndValues...)...)
    	}
    }
    
    func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) {  // 果然有
    	keysAndValues = formatTimes(keysAndValues)
    	pl.logger.Printf(
    		formatString(len(keysAndValues)+2),
    		append([]interface{}{msg, "error", err}, keysAndValues...)...)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    自此我们日志系统分析完成。

    1.3.3 cron支持zap日志

    因为我们项目一般都是使用zap做为日志管理,那zap能不能作为cron的日志呢?

    答案明显是可以的,下面我来写一个demo:

    package main
    
    // 需要拉取项目:go get github.com/robfig/cron/v3@v3.0.0
    
    import (
    	"fmt"
    	"github.com/robfig/cron/v3"
    	"go.uber.org/zap"
    	"time"
    )
    
    type MyZap struct {
    	logger *zap.Logger
    }
    
    // 自己把zap封装了一层,然后自己实现了Info接口
    func (this *MyZap)Info(format string,  keysAndValues ...interface{}) {
    	this.logger.Info(fmt.Sprintf(format, keysAndValues))
    }
    
    // 这也是自己实现了Error接口
    func (this *MyZap)Error(err error, format string,  keysAndValues ...interface{}) {
    	this.logger.Error(fmt.Sprintf(format, keysAndValues))
    }
    
    // new了一个对象
    func NewMyzap() *MyZap{
    	mzap := &MyZap{}
    	mzap.logger, _ = zap.NewProduction()
    	return mzap
    }
    
    func main() {
    	fmt.Println("time ", time.Now())
    
    	mzap := NewMyzap()
    	c := cron.New(cron.WithSeconds(), cron.WithLogger(mzap))  // 直接把mzap对象指针传入,不用那个转函数函数了,因为我直接实现了Logger接口
    	c.AddFunc("*/1 * * * * *", func() { fmt.Println("log ", mylog.buff) })
    	c.Start()
    
    	select {}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    这个代码其实只是一个简单的Demo,实际项目上不会这么用的,只是在这样提供了一种方法。(主要是真实项目也不敢上传,哈哈哈)

    1.4 总结

    cron的使用和一些简单的指定cron的参数就介绍到这里,最后还顺带介绍了一下日志系统,因为这个日志系统比较简单。明天继续。

  • 相关阅读:
    AI图书推荐:利用生成式AI实现业务流程超自动化
    【红队】ATT&CK - 自启动 - 注册表运行键、启动文件夹
    【架构】常见技术点--服务治理
    常用日志解决方案实践与学习-基于AOP利用注解实现日志记录原理分析
    由于找不到emp.dll无法继续执行此代码问题的五个解决方法
    八、动态规划(Dynamic Programming)
    运维监控Grafana部署
    NetCore配置详解(2)
    所有 WCF 超时说明
    语言主要是一种交流工具,而不是思维工具?GPT5何去何从?
  • 原文地址:https://blog.csdn.net/C1033177205/article/details/127680135