Skip to content
← 返回Go列表
开发实践

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 .。

go
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")
}

预期输出:

text
tick
tick
done

Ticker.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 .,程序约一秒后结束。

go
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")
    }
}

预期输出:

text
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 返回的完成信号才确认在途任务已退出。超时分支是失败上限,不是用延时保证成功。

依据:time、Go 1.23 Timer 变化、cron v3.0.1 文档。

继续阅读