Go: 定时任务

2023年 10月 2日 65.4k 0

写在前面

记录 Go 的一些关于定时任务的处理

内容

基础库

for + sleep

func main() {
	for {
		time.Sleep(5 * time.Second)
		fmt.Println("every 5s")
	}
}

Ticker

func main() {
	ticker := time.NewTicker(5 * time.Second)
	for range ticker.C {
		fmt.Println("every 5s")
	}
}

第三方库

cron

前面两个都是基础库里的东西,可以实现一些基本的定时功能,但对于一些稍显复杂的定时任务需求,就需要我们自己去做更多的内容来支持,比如说需求需要在每个月的 1 号,或是每周一之类的来进行任务,就需要我们自己去写代码来判断了。而 cron 这个第三方库,则能很好地支撑我们这类的需求。

安装

go get github.com/robfig/cron/v3@v3.0.0

cron 对于定时的设置有几种写法,都能达到同样的目的。

@表达式

func descriptors() {

	c := cron.New()
	// @every
	// 支持秒级别,会在启动 6s 后执行
	_, errEveryS := c.AddFunc("@every 6s", func() {
		fmt.Println("every 6s")
	})
	if errEveryS != nil {
		fmt.Printf("errEveryS: %v\n", errEveryS)
	}
	// 0.1 min = 6s
	_, errEveryM := c.AddFunc("@every 0.1m", func() {
		fmt.Println("every 0.1m")
	})

	if errEveryM != nil {
		fmt.Printf("errEveryM: %v\n", errEveryM)
	}

	// @yearly @annually 每年执行一次,即 1 月 1 日的时候开始执行
	_, errYearly := c.AddFunc("@yearly", func() {
		fmt.Println("yearly")
	})
	if errYearly != nil {
		fmt.Printf("errYearly: %v\n", errYearly)
	}

	_, errAnnually := c.AddFunc("@annually", func() {
		fmt.Println("annually")
	})
	if errAnnually != nil {
		fmt.Printf("errAnnually: %v\n", errAnnually)
	}

	// @monthly 每个月的第一天的零点执行
	_, errMonthly := c.AddFunc("@monthly", func() {
		fmt.Println("monthly")
	})
	if errMonthly != nil {
		fmt.Printf("errMonthly: %v\n", errMonthly)
	}

	// @weekly 每周的第一天零点执行,这个第一天可能是周六,也可能是周日
	_, errWeekly := c.AddFunc("@weekly", func() {
		fmt.Println("weekly")
	})
	if errWeekly != nil {
		fmt.Printf("errWeekly: %v\n", errWeekly)
	}

	// @hourly 每小时执行一次,启动后一小时执行
	_, errHourly := c.AddFunc("@hourly", func() {
		fmt.Println("hourly")
	})
	if errHourly != nil {
		fmt.Printf("errHourly: %v\n", errHourly)
	}

	// @daily @midnight,每天执行一次,在每天的零点执行
	_, errDaily := c.AddFunc("@daily", func() {
		fmt.Println("daily")
	})
	if errDaily != nil {
		fmt.Printf("errDaily: %v\n", errDaily)
	}

	_, errMidnight := c.AddFunc("@midnight", func() {
		fmt.Println("midnight")
	})
	if errMidnight != nil {
		fmt.Printf("errMidnight: %v\n", errMidnight)
	}

	c.Start()
	defer c.Stop()

	select {}
}

linux cron 表达式

如果我们想要更灵活的一些处理,就需要学习下 Linux 的 cron 表达式,参照如下:

    *    *    *    *    *
    -    -    -    -    -
    |    |    |    |    |
    |    |    |    |    +----- day of week (0 - 7) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
    |    |    |    +---------- month (1 - 12) OR jan,feb,mar,apr ...
    |    |    +--------------- day of month (1 - 31)
    |    +-------------------- hour (0 - 23)
    +------------------------- minute (0 - 59)

只要我们学会了 Linux 的 cron 表达式的使用,也就可以直接用在 cron 库上,我们可以看到 Linux cron 表达式的标准实现里,最小的时间单位是支持到分钟级别,秒级别的话我们可以用上面的方案处理。

func linux() {
	c := cron.New()

	// 每1分钟执行,等同于 */1 * * * * 和 @every 1m
	_, err := c.AddFunc("* * * * *", func() {
		fmt.Println("* * * * *")
	})
	if err != nil {
		fmt.Printf("err: %v\n", err)
	}

	// 每 5 分钟执行,等同于 @every 5m
	// / 表示频率
	_, err1 := c.AddFunc("*/5 * * * *", func() {
		fmt.Println("* * * * *")
	})
	if err1 != nil {
		fmt.Printf("err1: %v\n", err1)
	}

	// 每个小时的 01 分执行
	_, err2 := c.AddFunc("1 * * * *", func() {
		fmt.Println("1 * * * *")
	})
	if err2 != nil {
		fmt.Printf("err2: %v\n", err2)
	}

	// 每个月的第一天的 0 点 0 分运行,相当于 @monthly
	_, err3 := c.AddFunc("0 0 1 * *", func() {
		fmt.Println("0 0 1 * *")
	})
	if err3 != nil {
		fmt.Printf("err3: %v\n", err3)
	}

	// 每周的第一天(周六或周日)的 0 点 0 分运行,相当于 @weekly
	// 像一些发奖需求,在每周一进行发奖,就可以写成 0 0 * * 1
	_, err4 := c.AddFunc("0 0 * * 0", func() {
		fmt.Println("0 0 * * 0")
	})
	if err4 != nil {
		fmt.Printf("err4: %v\n", err4)
	}

	// 每天的 0 点 0 分运行一次,相当于 @daily 和 @midnight
	_, err5 := c.AddFunc("0 0 * * *", func() {
		fmt.Println("0 0 * * *")
	})
	if err5 != nil {
		fmt.Printf("err5: %v\n", err5)
	}

	// 每年运行一次,在 1 月 1 日 0 点 0 分,相当于 @yearly @annually
	_, err6 := c.AddFunc("0 0 1 1 *", func() {
		fmt.Println("0 0 1 1 *")
	})
	if err6 != nil {
		fmt.Printf("err6: %v\n", err6)
	}

	// 每小时运行一次,相当于 @hourly
	_, err7 := c.AddFunc("0 * * * *", func() {
		fmt.Println("0 * * * *")
	})
	if err7 != nil {
		fmt.Printf("err7: %v\n", err7)
	}

	// - 表示范围 , 表示列表
	// 这里的意思是,在每天的 3 点到 6 点和 20 点到 23 点这两个范围内,每个 30 分执行一次
	// 也就是 3 点 30,4 点 30,5 点 30 这样
	_, err8 := c.AddFunc("30 3-6,20-23 * * *", func() {
		fmt.Println("30 3-6,20-23 * * *")
	})
	if err8 != nil {
		fmt.Printf("err8: %v\n", err8)
	}

	c.Start()
	defer c.Stop()

	select {}
}

更进一步的,假如我们在做海外的应用,有时需要根据时区来进行定时任务,那么可以这么写:

func timezone() {
	c := cron.New()

	// 在东京时区,每天的 4 点 30 分运行
	_, err := c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() {
		fmt.Println("CRON_TZ=Asia/Tokyo 30 04 * * *")
	})
	if err != nil {
		fmt.Printf("err: %v\n", err)
	}

	c.Start()
	defer c.Stop()

	// 或者在 cron 初始化的时候指定时区
	tc, _ := time.LoadLocation("Asia/Tokyo")
	c1 := cron.New(cron.WithLocation(tc))
	_, err1 := c1.AddFunc("30 04 * * *", func() {
		fmt.Println("30 04 * * *")
	})
	if err1 != nil {
		fmt.Printf("err1: %v\n", err1)
	}

	c1.Start()
	defer c1.Stop()

	select {}
}

job

在工作中,为了做到封装的效果,我们可以实现 Job 接口,把任务给封装起来:

func job() {
	c := cron.New()

	j1 := MyJob{name: "1"}
	_, errJob1 := c.AddJob("@every 5s", &j1)
	if errJob1 != nil {
		fmt.Printf("errJob1: %v", errJob1)
	}

	j2 := MyJob{name: "2"}
	_, errJob2 := c.AddJob("@every 6s", &j2)
	if errJob2 != nil {
		fmt.Printf("errJob2: %v", errJob2)
	}
	c.Start()
	select {}
}

// MyJob 对定时任务进行包装
type MyJob struct {
	name string
}

// Run 实现 Job 接口里的 Run 方法
func (j *MyJob) Run() {
	fmt.Printf("myjob : %s\n", j.name)
}

schedule

如果我们想动态的调整任务的下一次运行的时间,就可以通过实现 Schedule 接口来处理:

func schedule() {
	c := cron.New()

    // 为了方便,这里用 FuncJob,就不去创建 Job 结构体了 
	c.Schedule(&MySchedule{}, cron.FuncJob(func() {
		fmt.Println("hello")
	}))

	c.Start()

	select {}

}

type MySchedule struct {
}

// Next 实现 Schedule 接口的 Next 方法
// Start 的时候,会触发 Next 方法,我们可以在这个的基础上,返回下一次的运行时间
// 之后每次触发都会调用 Next,我们就可以在这里做文章,动态修改下一次的触发时间
func (s *MySchedule) Next(t time.Time) time.Time {
	fmt.Printf("now time: %v\n", t)
	result := t.Add(5 * time.Second)
	return result
}

参考

crontab执行时间计算

cron github

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论