用Go编写一个简单的定时器,首先需要考虑定时器需要实现哪些功能?
- 定时执行
- 拦截执行错误
- 批量添加执行,检测执行存活
- 批量停止执行,检测执行停止
定义结构体
要解决我们第1.3.4点的需求,需要定义一个结构体,用于存储任务信息,以及任务状态
type _Job struct {
// 名称
Name string
// 是否运行
Running bool
// 执行间隔
Ticker *time.Ticker
// 任务方法
Call func()
// 是否已关机
IsShutdown bool
}
在执行过程中,通过不断获取结构体中的信息,来检测状态以及执行任务
初始化任务
为了在程序启动时自动开始任务,我们定义了一个jobMap用来全局存储任务状态,以及定义了一个初始化函数initJob,并在其中添加了个任务myJob。这些任务会以一定的频率(例如每分钟一次)执行。如果你不希望系统执行任务,可以在配置中关闭它。
var jobMap sync.Map
func initJob() {
if !global.Config.System.Job {
return
}
fmt.Println("========start job =================")
myJob(time.Minute * 1)
}
func myJob(d time.Duration) {
addJob(d, func() {
if err := job_service.exampleJob(); err != nil {
global.Logger.Error("myJob_exampleJob_err:", zap.Error(err))
}
})
}
添加任务
为了批量添加任务,我们定义了一个addJob函数,它接受一个执行间隔和一个函数作为参数,将它们封装为一个_Job实例,并将实例存储在jobMap中。然后,这个函数启动一个新的goroutine来执行任务。
func addJob(d time.Duration, call func()) {
ticker := time.NewTicker(d)
name := runtime.FuncForPC(reflect.ValueOf(call).Pointer()).Name()
job := _Job{
Name: name,
Running: false,
Ticker: ticker,
Call: call,
IsShutdown: false,
}
jobMap.Store(name, job)
go func(do _Job) {
defer func() {
if err := recover(); err != nil {
global.Logger.Error("job_goroutine_error",
zap.Any("error msg:", err),
zap.Stack("addJob_goroutine_stack"),
)
}
}()
for range ticker.C {
v, _ := jobMap.Load(do.Name)
runJob := v.(_Job)
if runJob.IsShutdown == true {
return
}
runJob.Running = true
jobMap.Store(do.Name, runJob)
do.Call()
runJob.Running = false
jobMap.Store(do.Name, runJob)
}
}(job)
}
停止任务
在某些情况下,我们可能需要停止正在运行的任务。例如,当我们想要关闭系统时,我们需要优雅地关闭所有正在运行的任务。为此,我们定义了一个stopJob函数,它将遍历jobMap中的所有任务,停止它们的定时器,并将IsShutdown字段设置为true。
func stopJob() {
fmt.Println("========stop job =================")
jobList := make([]_Job, 0)
jobMap.Range(func(k, v interface{}) bool {
jobList = append(jobList, v.(_Job))
return true
})
// 先停止定时器
for i := range jobList {
job := jobList[i]
job.Ticker.Stop()
job.IsShutdown = true
jobMap.Store(job.Name, job)
}
// 查看定时器是否在运行
// 等待运行中结束
for i := range jobList {
for {
v, _ := jobMap.Load(jobList[i].Name)
job := v.(_Job)
if !job.Running {
break
}
global.Logger.Info("job is running", zap.String("name", job.Name))
// 睡眠一秒
time.Sleep(time.Second)
}
}
}
这种方式的定时任务设计,使得我们可以很方便地对任务进行控制和管理,同时能够保证任务的正常执行和优雅的停止。
异常捕获
在执行定时任务时,我们不可避免地会遇到一些错误或异常。为了避免一个任务的异常影响到其他任务或者整个程序,我们需要对每个任务的执行进行异常捕获。这个逻辑在我们的 addJob 函数的 goroutine 中实现。
go func(do _Job) {
defer func() {
if err := recover(); err != nil {
global.Logger.Error("job_goroutine_error",
zap.Any("error msg:", err),
zap.Stack("addJob_goroutine_stack"),
)
}
}()
// ....
}(job)
在这段代码中,我们使用 defer 和 recover 函数来捕获可能产生的 panic。如果有 panic 发生,我们就记录下来,并打印相应的堆栈信息。
总结
在此,我们实现了一个简单的定时任务系统,它包括:
- 使用
sync.Map保存所有任务 - 定义
_Job结构体用来描述一个任务 - 提供
initJob函数来启动任务 - 提供
addJob函数来新增任务 - 提供
stopJob函数来停止所有任务 - 异常捕获以保证任务出错时不会影响整个程序的运行
这个系统还有很多可以改进的地方。比如,我们可以增加任务的删除和修改功能,可以提供统一的任务错误处理机制,可以增加任务的优先级设置,等等。但是,基本的设计思路和实现方式已经在这里了,你可以根据你的需要进行修改和扩展。