Go 定时任务:Timer、Ticker、cron 与退出
前置:channel 与取消、时间解析。一次延时用 Timer,固定间隔用 Ticker,按某时区的日历表达式运行可用 cron。Go 1.27.2;cron 示例固定 v3.0.1,本轮仅推导 tick、解析结果与退出协议,未安装依赖或运行示例;macOS 仅作平台说明,本项目不执行平台运行验证。
Ticker Stop 不会关闭通道
下面的独立程序只处理两个 tick 后返回,退出由循环条件决定。保存为新模块的 main.go,执行 go run .。
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for n := 0; n < 2; n++ {
<-ticker.C
fmt.Println("tick")
}
fmt.Println("done")
}预期输出:
tick
tick
doneTicker.Stop 停止后续 tick,却不关闭 ticker.C,因此 for range ticker.C 不会因 Stop 自动返回。长期任务用 select 同时监听 ctx.Done,并等待工作函数退出。消费者慢时可能丢 tick;需要“处理完成后再等一段时间”时,应在完成后重新安排 Timer。
Timer 与版本边界
NewTimer 一次触发;time.After 适合一次性等待。Go 1.23 改变了通道计时器和不可达 Timer/Ticker 的回收规则,是否采用新行为还受主模块 go 行和兼容设置影响。不能将旧版“必须 drain”步骤或“time.After 永远泄漏”当通用规则。仍有 goroutine 阻塞在接收上时,垃圾回收也不会替你完成退出协议。
AfterFunc 的回调在独立 goroutine 执行,Stop 不等待已开始的回调结束。关闭顺序应明确:停止创建新工作、通知取消、等待在途工作结束。
cron 字段、星期与时区
在另一个空目录执行 go mod init example.com/schedule,然后执行 go get github.com/robfig/cron/v3@v3.0.1。这是显式的教学版本,不是自动升级建议。保存下列 main.go 后执行 go run .,程序约一秒后结束。
package main
import (
"context"
"fmt"
"sync"
"time"
"github.com/robfig/cron/v3"
)
func main() {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
schedule, err := parser.Parse("CRON_TZ=UTC 30 9 * * 1")
if err != nil { panic(err) }
from := time.Date(2026, 10, 17, 0, 0, 0, 0, time.UTC)
fmt.Println(schedule.Next(from).Format(time.RFC3339))
_, err = parser.Parse("0 30 9 * * 1")
fmt.Println("six fields rejected:", err != nil)
done := make(chan struct{})
var once sync.Once
worker := cron.New(cron.WithLocation(time.UTC))
if _, err := worker.AddFunc("@every 1s", func() {
once.Do(func() { fmt.Println("job"); close(done) })
}); err != nil { panic(err) }
worker.Start()
select {
case <-done:
case <-time.After(5 * time.Second): panic("job did not run")
}
stopped := worker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case <-stopped.Done(): fmt.Println("stopped")
case <-ctx.Done(): panic("job did not stop")
}
}预期输出:
2026-10-19T09:30:00Z
six fields rejected: true
job
stopped默认五字段是“分、时、日、月、星期”;cron.WithSeconds 或显式包含 Second 的解析器才接受六字段。星期 0 是周日、1 是周一,旧笔记将 0 写成周六是错误的。@every 表达间隔,CRON_TZ 表达时区;每天某地 9 点不等于每隔 24 小时,夏令时跳变还可能导致该次计划不运行。可用 _ "time/tzdata" 随程序携带时区数据,或在部署环境提供时区数据库。
重叠、错误与停止
AddFunc 的解析错误必须处理。默认任务可能重叠,需要限制时显式配置 SkipIfStillRunning 或 DelayIfStillRunning;不要以为调度器自动保证只运行一个。需要恢复任务 panic 时显式配置 Recover,不能假设默认恢复。cron.Stop 停止调度并返回可等待在途任务的 context,不会强制取消业务函数;长任务自身还需接收取消或期限。
这是单进程内调度,不提供多实例唯一执行、持久化或故障补偿,不能把本例作为分布式调度方案。
练习与自检
把表达式改为周日,预计 Next 为 2026-10-18;再在默认五字段解析器中添加秒字段,确认报错。解释 Stop 为什么需要等待,而 Sleep 三秒无法保证任务完成。
参考解法
使用星期 0;五字段解析器拒绝六字段。Sleep 仅等待时间经过,Stop 返回的完成信号才确认在途任务已退出。超时分支是失败上限,不是用延时保证成功。
继续阅读
- 开发实践
- 知识专题
- 知识专题