应使用 time.Ticker 实现固定间隔调度,因其可精确、可控、无 goroutine 泄漏;time.AfterFunc 嵌套调用会导致 goroutine 累积、精度丢失、无法优雅停止,易触发栈溢出或调度延迟。

用 time.Ticker 做固定间隔调度,别碰 time.AfterFunc 嵌套
固定周期执行任务(比如每 5 秒拉一次配置),time.Ticker 是唯一靠谱选择。time.AfterFunc 看似简单,但每次回调里再调自己,会累积 goroutine、丢失精度、无法优雅停止。
常见错误现象:runtime: goroutine stack exceeds 1000000000-byte limit 或调度越来越慢——本质是没清理旧 timer,新 timer 不断 spawn。
- 始终用
ticker := time.NewTicker(5 * time.Second)初始化,不是time.After - 在
for range ticker.C循环里处理业务,别用select+case 模拟 - 必须显式调用
ticker.Stop(),尤其在 long-running service 中,否则 ticker 持有 channel 不释放,GC 不掉
time.Parse 解析 cron 表达式?别硬刚,先拆解成 time.Duration
Golang 标准库没有 cron 解析器,time.Parse 只认 RFC3339/ANSI C 格式,不支持 "0 */2 * * *" 这类字符串。强行写 parser 容易漏掉月份天数差异、夏令时、闰秒等边界。
使用场景:你只需要“每天 9 点”“每小时整点”,而不是完整 cron 功能。
立即学习“go语言免费学习笔记(深入)”;
- 用
time.Parse("15:04", "09:00")提取目标时分,再算出下次触发的time.Time - 把 “每 30 分钟” 转成
30 * time.Minute,交给time.Ticker;把 “每周一凌晨” 转成带time.Weekday的计算逻辑 - 如果真要支持标准 cron,直接上
robfig/cron/v3,别重复造轮子——它的cron.NewScheduler底层仍是time.Timer和时间差计算,但已覆盖 DST、跨月等 case
并发安全:任务函数里改共享变量,得加 sync.Mutex 或用 atomic
多个 ticker 触发或单个 ticker 并发执行(比如用了 go task()),共享状态如计数器、缓存 map、开关标志,立刻出现 data race。
错误现象:fatal error: concurrent map writes 或数值随机错乱,go run -race 一跑就报。
- 读写 map / struct 字段 / slice,统一包一层
sync.RWMutex,别只锁写不锁读 - 纯整数计数(如执行次数)优先用
atomic.Int64,比 mutex 轻量,且不会死锁 - 避免在 ticker 回调里直接启动长任务 goroutine 后不管——用
errgroup.Group或带超时的context.WithTimeout控制生命周期
测试调度逻辑:别 sleep 等真实时间,用 clock.WithTestClock 或接口抽象
写 test 时用 time.Sleep(5 * time.Second) 等 ticker 触发,既慢又不稳定。CI 上可能因负载高而超时,本地也浪费时间。
根本解法是把时间依赖抽成接口,例如定义 type Clock interface { Now() time.Time; After(d time.Duration) ,生产用 realClock,测试用可快进的 mock clock。
- 社区常用方案是
github.com/benbjohnson/clock,它的clock.NewMock()支持tick.Advance(10 * time.Second)手动推进时间 - 不要在测试里 patch
time.Now—— Go 不支持可靠 monkey patch,容易漏掉其他 time 函数调用 - 验证点聚焦在:是否按预期时间点触发、是否跳过阻塞期(如任务耗时 > 间隔)、Stop 后 channel 是否关闭
真正麻烦的是时间语义本身:UTC vs 本地时区、夏令时切换那一个小时会发生什么、机器时间被 NTP 突然校正……这些不是加个 Mutex 就能解决的,得看你的业务能不能容忍“最多晚 1 分钟执行”。










