go标准库用time.ticker和time.timer即可实现轻量调度器:ticker用于固定间隔周期任务,timer用于单次延迟执行;需配合context控制生命周期、唯一id管理任务状态、unixmilli()精确比较时间,并注意gocron等库的参数绑定与限速陷阱。

用 time.Ticker 和 time.Timer 实现最简周期/延迟调度
不需要任何第三方库,Go 标准库就能跑起来一个能用的调度器。核心就两个:用 time.Ticker 做周期触发,用 time.Timer 做单次延迟。别一上来就搞复杂状态管理——先让任务能准时跑起来再说。
-
time.Ticker适合固定间隔任务(比如每 5 秒拉一次接口),但注意它不感知任务执行耗时;如果任务本身比间隔长,会堆积或跳过(取决于你是否阻塞读ticker.C) -
time.Timer更适合“延后执行”,比如发短信失败后 30 秒重试;别重复调Reset(),容易漏掉已触发的C事件,推荐每次新建一个Timer - 所有 goroutine 必须带
ctx.Done()检查,否则Shutdown()时可能卡死;尤其避免在Job函数里起子 goroutine 却不绑定父 ctx
封装 Scheduler 结构体时必须处理的三个状态问题
光有定时器不够,任务要可注册、可取消、可查状态。一个没状态管理的调度器,在生产环境等于裸奔。
- 每个
Task必须自带唯一ID字段,否则无法Remove()或Pause();别用函数名当 ID,同名函数可能来自不同包 -
nextRun时间戳别存为time.Time直接比较——浮点误差和时区会让判断出错;统一转成UnixMilli()整数做比较更稳 - 启动/停止不是开关按钮:调用
Stop()后要等所有正在运行的Job自然退出(靠 ctx 取消),再 close channel;直接 close 调度 channel 会导致 panic
用 gocron/v2 时最容易踩的坑:参数绑定和上下文传递
gocron 看似简单,但传参和 ctx 处理稍不注意就静默失败。
-
NewTask()的参数必须和函数签名严格匹配;func(ctx context.Context, s string)不能传"hello"就完事,第一个参数必须是context.Context类型,否则 runtime panic - 别把
context.Background()当万能兜底;真实场景要用带 timeout 的context.WithTimeout(),否则超时任务永远卡着不退出 -
DurationJob和CronJob的时间基准不同:DurationJob从上次执行结束开始算间隔,CronJob是绝对时间点对齐;混用会导致任务漂移
并发任务限速时,rate.Limiter 比 time.Sleep 更可靠
想控制每秒最多执行 10 个任务?别用 time.Sleep(100 * time.Millisecond) ——它只管“睡”,不管“有没有任务来”。
-
rate.NewLimiter(rate.Every(time.Second/10), 1)才是正解:第一个参数是平均速率,第二个是突发容量(burst);burst=1 表示绝不允许并行,burst=5 表示最多攒 5 个一起放行 - 务必在
Submit()之前调用limiter.Wait(ctx),而不是在 job 函数里调;否则限速逻辑被绕过,worker pool 会瞬间打满 - 注意
rate.Limiter不是线程安全的,多个 goroutine 共用同一个实例没问题,但别在不同 goroutine 里反复 new 它
真正难的不是让任务跑起来,而是让它们“跑得明白”:谁在跑、跑到了哪一步、失败了怎么恢复、重启后会不会丢任务。这些细节不写进代码里,早晚变成半夜告警的源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











