time.Ticker 仅适用于秒级、低频、无状态的轻量任务,如每30秒健康检查;涉及超时、重试、并发或失败补偿时应改用 robfig/cron/v3,并注意时区设置与 context 透传。

用 time.Ticker 做简单定时爬取,但别在生产环境直接这么干
它适合秒级、低频、无状态的轻量任务,比如每 30 秒拉一次健康检查接口。但一旦涉及网络超时、重试、并发控制或任务失败后需补偿,time.Ticker 就会暴露问题:goroutine 泄漏、时间漂移、无法暂停/取消、错误不落地。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
time.NewTicker启动后,务必在defer ticker.Stop()配合select+ctx.Done()实现可控退出 - 每次爬取必须包在
go func() { ... }()里并加 recover,否则 panic 会 kill 整个 ticker 循环 - 别把 HTTP 客户端复用逻辑写在 ticker 循环内——连接池、超时、重试应提前配置好,例如:
http.Client{Timeout: 10 * time.Second}
用 robfig/cron/v3 管理复杂调度,但注意它的默认时区和上下文传递
这是目前最稳定的 Go 定时库,支持 cron 表达式、Job 接口、运行时增删任务。但它默认使用本地时区(不是 UTC),且原生不透传 context.Context 到 Job 执行函数中——这意味着你没法做超时控制或主动取消正在爬取的任务。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 初始化 cron 实例时显式指定时区:
c := cron.New(cron.WithLocation(time.UTC)) - 封装一层 wrapper,把
context.Context注入到实际爬取逻辑中,例如:c.AddFunc("0 */2 * * *", func() { crawlWithCtx(ctx) }) - 避免在
AddFunc中直接写长耗时逻辑;改用启动 goroutine + channel 控制并发数,防止任务堆积 - 如果需要任务幂等性(比如防止同一时间窗口重复抓取),得自己加分布式锁或时间窗口校验,库本身不提供
爬取失败时怎么重试?别只靠 for i := 0; i
固定次数重试在弱网环境下容易失败,而无限重试又可能卡死整个 job。真正的重试策略要结合退避(backoff)、错误分类、可观测性三者。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
github.com/cenkalti/backoff/v4替代手写 for 循环,例如指数退避:backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3) - 只对可重试错误重试(如
net.ErrTimeout、502/503/504),对401、404或 JSON 解析失败这类业务错误立即终止 - 每次重试前记录日志,带上尝试次数、当前 backoff 间隔、原始错误,方便排查是瞬时抖动还是目标接口已变更
- 如果爬取目标有反爬机制,重试时记得轮换
User-Agent或加随机 delay,否则容易被封 IP
如何避免爬虫把目标服务器打挂?控制并发和请求节奏是硬指标
哪怕单次请求很轻,高频+高并发也会触发目标风控或压垮其负载均衡器。Go 的 goroutine 轻量不等于可以无节制并发。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用带缓冲的 channel 做信号量控制并发数,例如:
sem := make(chan struct{}, 5),每次请求前sem ,结束后 - 对同一域名的所有请求强制走同一个
http.Client实例,并设置Transport.MaxIdleConnsPerHost = 20,避免新建连接风暴 - 如果爬多个页面,别用
time.Sleep硬等,改用time.After+select实现“每个请求至少间隔 N 秒”的软限流 - 上线前先用
ab或hey对自己的爬取模块压测,观察 goroutine 数、内存增长、GC 频率是否异常
真正难的不是写一个能跑起来的定时爬虫,而是让这个爬虫在连续运行 30 天后,依然能准确识别出目标页面结构变化、自动绕过新出现的验证码、并在自身某次 panic 后不丢失上下文状态——这些都得靠日志、指标、降级开关和人工干预通道来兜底,而不是靠某个库的“高级特性”。










