Go中time.Ticker/AfterFunc不感知context,需用select监听ctx.Done()并配合ticker.Stop()防泄漏;递归调度应检查ctx.Err()避免误触发。

定时任务里直接传 Context 会失效
Go 的 time.Ticker 和 time.AfterFunc 本身不接受 context.Context,硬塞进去(比如闭包捕获)会导致超时或取消信号无法穿透到任务执行体内部——你调了 ctx.Done(),但任务还在跑。
用 context.WithCancel + select 主动监听取消
核心思路是:把定时逻辑包进一个长生命周期的 goroutine,在每次 tick 后用 select 同时等待 ctx.Done() 和任务执行完成。这样取消信号能立刻中断下一轮调度。
常见错误写法:
// ❌ 错误:没检查 ctx,cancel 后仍会执行下一次
ticker := time.NewTicker(5 * time.Second)
go func() {
for range ticker.C {
doWork()
}
}()
正确做法:
// ✅ 正确:每次循环都 select ctx
func runPeriodic(ctx context.Context, interval time.Duration, fn func()) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
<pre class="brush:php;toolbar:false;">for {
select {
case <p>}</p><p>// 使用
ctx, cancel := context.WithTimeout(context.Background(), 30<em>time.Second)
defer cancel()
runPeriodic(ctx, 5</em>time.Second, doWork)</p>
- 必须调用
ticker.Stop(),否则即使 goroutine 退出,ticker仍泄漏 - 如果
fn()执行时间 >interval,下一次 tick 会“堆积”,select仍只触发一次,不会并发重入 - 若需防止任务堆积,改用
time.After+ 递归调度(见下一条)
需要防堆积?用 time.After + 递归 + context.WithTimeout
当任务耗时不可控,又不想多个实例并发执行(比如发 HTTP 请求、写文件),应避免 time.Ticker,改用单次延时+手动重启。
func runOnceThenDelay(ctx context.Context, delay time.Duration, fn func(context.Context) error) {
if ctx.Err() != nil {
return
}
<pre class="brush:php;toolbar:false;">// 给本次执行单独加超时(可选)
execCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := fn(execCtx); err != nil {
log.Printf("task failed: %v", err)
}
// 只有成功才继续下一轮
select {
case <p>}</p>
- 递归调用前必须检查
ctx.Err(),否则 cancel 后仍会进入下一层 - 任务函数
fn应接收context.Context并主动响应Done(),比如传给http.Client.Do - 不用
time.Ticker就不存在“漏 tick”问题,但要注意递归深度——实际中用循环更稳妥(可自行展开)
第三方库如 robfig/cron 怎么传 Context
robfig/cron(v3)原生不支持 context.Context,它的 Job 接口只有 Run() 方法,无参数。强行传需靠闭包捕获,但取消不可靠。
推荐方案:
- 升级到
github.com/robfig/cron/v3并启用WithChain+ 自定义 wrapper,但 wrapper 无法终止正在运行的Run() - 更稳妥的做法:在
Run()内部生成带超时的子 context,例如childCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second),再传给具体业务逻辑 - 若需全局取消所有 cron job,得自己维护一组
context.CancelFunc,并在 shutdown 时统一调用
真正难的不是怎么传,而是任务内部是否尊重 context —— 比如用 time.Sleep 而非 time.After 等待,或 HTTP 客户端没设 Timeout,都会让 cancel 形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











