可插拔抓取器需定义最小fetch接口并依赖抽象而非实现,配置内聚于结构体避免全局变量,url去重分内存层sync.map与持久层redis,解析器按域名或content-type动态路由注册。

Go 语言本身不提供“面向未来”或“高扩展性”的魔法,但用对模式、约束好边界、提前预留扩展点,就能让爬虫模块在需求变更时少改代码、不重写。
如何设计可插拔的抓取器(Fetcher)接口
硬编码 HTTP 客户端或直接调用 http.Get 会让后续加代理、重试、限速、Mock 测试变得困难。必须抽象出 Fetcher 接口:
- 定义最小契约:
Fetch(ctx context.Context, url string) (*http.Response, error) - 所有具体实现(如带 retry 的
RetryFetcher、带 UA 轮换的RotatingFetcher)都实现该接口 - 主逻辑只依赖接口,不 import 具体实现包 —— 这是解耦关键
- 避免在接口里塞太多方法(比如
Close()或SetTimeout()),否则违反接口隔离原则
为什么用结构体字段而非全局变量来管理配置
全局变量(如 var MaxConcurrentRequests = 10)会导致测试难 mock、多实例无法独立配置、并发 unsafe。正确做法是把配置内聚进结构体:
- 定义
Crawler结构体,字段包含fetcher Fetcher、rateLimiter Limiter、maxDepth int等 - 构造函数接收配置参数,不做默认值“猜测”,强制调用方显式传参:
NewCrawler(WithFetcher(f), WithRateLimiter(l)) - 每个
Crawler实例彼此隔离,可同时跑不同策略的爬取任务
如何安全地处理 URL 去重与状态追踪
用 map[string]bool 存已访问 URL 在小规模下可行,但内存随 URL 数量线性增长,且无法跨进程/重启持久化。真实场景应分层处理:
- 内存层:用
sync.Map做快速去重(key 是标准化后的url.String()),适合单机高频判断 - 持久层:对接 Redis 或本地 BoltDB,存
url → status(如 “pending”, “success”, “failed”),用于断点续爬 - 注意 URL 标准化:去掉 fragment、统一 scheme、规范 query 参数顺序(否则
a.com/?b=1&a=2和a.com/?a=2&b=1会被当成两个 URL) - 避免直接用原始
*url.URL做 map key —— 它不可比较,会 panic
如何让解析器(Parser)支持动态注册而不改主流程
当新增网站需要定制解析逻辑时,不应修改爬虫核心代码。方案是基于内容类型(Content-Type)或域名前缀做路由:
- 定义
Parser接口:Parse(body io.Reader, url *url.URL) ([]Item, error) - 维护一个
map[string]Parser,key 可以是域名("github.com")、路径前缀("/blog/")或 MIME 类型("text/html") - 注册时用函数选项模式:
RegisterParser("example.com", &ExampleComParser{}) - 主流程中根据响应头
Content-Type或url.Host查找对应Parser,找不到则用默认HTMLParser
扩展性不是堆功能,而是控制变化半径 —— 新增一个网站解析逻辑,只需实现一个 Parser 并注册,其他模块完全不动。最容易被忽略的是 URL 标准化和状态持久化的衔接点:如果两次爬取之间没保存中间状态,或者 URL 没归一化,所谓“可扩展”就只是幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











