Go Colly OnError 中如何区分传输失败与 HTTP 错误
我在用 Colly 抓取列表页时,最容易误判的一类日志是“请求失败”:有时是真正的 DNS、连接或超时问题,有时只是服务端返回了 404、429 或 500。两者都可能进入 OnError,但处理方式完全不同。实用的判断顺序是:先看 Response 是否存在,再看 StatusCode 是否代表 HTTP 状态异常,最后用 errors.As 和 errors.Is 判断传输错误的具体原因。
官方地址:https://github.com/gocolly/colly
不要用“err != nil”直接等同于网络断线。对于 HTTP 错误,Colly 通常仍能把状态码放在Response中;对于传输失败,响应可能为空。默认配置下,非 2xx 会触发OnError,如果启用ParseHTTPErrorResponse,则可以把非 2xx 当作可解析响应继续走响应处理。
Response != nil且StatusCode >= 300,优先按 HTTP 错误处理。Response == nil或没有有效状态码时,再检查*url.Error、超时和取消原因。- 不要把 4xx、5xx 无条件重试;先区分客户端请求、限流、服务端暂时故障和永久不存在。
先看 OnError 的两个参数
Colly 的错误回调类型是 func(*Response, error)。第一个参数不是“请求一定成功”的证明,而是 Colly 在能够拿到 HTTP 响应或已经建立部分响应上下文时提供的对象;第二个参数是当前阶段的错误。官方源码在错误处理时会把缺失的响应补成带有 Request 和 Context 的对象,但这不等于它拥有真实的状态码和响应体。

因此,日志代码不要只打印 err.Error()。先把 URL、方法、状态码和错误类型放到同一条记录里,后面才能回答“服务器拒绝了请求”还是“请求根本没有拿到响应”。
c := colly.NewCollector()
c.OnError(func(resp *colly.Response, err error) {
// Response 可能没有真实的 HTTP 状态,默认值不能被当成成功响应。
status := 0
requestURL := ""
if resp != nil {
status = resp.StatusCode
if resp.Request != nil && resp.Request.URL != nil {
requestURL = resp.Request.URL.String()
}
}
// 先记录原始 error,再按状态码决定它属于哪一类。
log.Printf("colly error url=%s status=%d err=%v", requestURL, status, err)
})
这里的 status == 0 只是“当前没有可用 HTTP 状态码”的日志标记,不是 Colly 定义的状态码。它可能对应连接失败,也可能对应请求检查、重定向或回调阶段的其他错误,仍要结合 error 判断。
用状态码和错误包装区分来源
我更愿意把分类拆成两层:第一层只回答“有没有 HTTP 响应”,第二层才回答“没有响应时,传输失败的具体原因是什么”。这样不会因为某个库把底层错误包了一层,就误把所有情况归到同一类。
func classifyCollyError(resp *colly.Response, err error) string {
// 只要拿到明确的非 2xx 状态,就先按 HTTP 错误归类。
if resp != nil && resp.StatusCode >= 300 {
return "http-error"
}
// url.Error 常见于 HTTP 客户端包装的请求失败,保留它便于日志展开。
var urlErr *url.Error
if errors.As(err, &urlErr) {
if errors.Is(urlErr.Err, context.DeadlineExceeded) {
return "transport-timeout"
}
return "transport-error"
}
// 未匹配到包装类型时仍保留一个通用类别,避免丢失未知错误。
if errors.Is(err, context.DeadlineExceeded) {
return "transport-timeout"
}
return "other-error"
}
这段判断的重点不是类别名称,而是顺序:HTTP 状态码优先于错误字符串;底层原因优先用 errors.Is 和 errors.As,不要依赖“timeout”“connection refused”这类可能变化的文本。对于 Response == nil 的情况,url.Error 往往能补充请求方法和目标地址。
还要留意一个容易忽略的边界:OnError 并不只可能接收网络阶段的错误。HTML/XML 处理回调返回错误时,Colly 也会调用错误处理路径;此时 Response 可能存在且状态码是 2xx。若业务要专门统计传输失败,应把“状态码分类”和“解析/业务回调错误”分开计数。
非 2xx 要不要继续读取响应体
默认情况下,Colly 只按成功响应继续处理;官方配置中的 ParseHTTPErrorResponse 用来允许解析非 2xx 响应。两种策略都合理,取决于目标站点把错误体当不当作业务数据。
// 需要读取 404/429/500 响应体时,显式开启非 2xx 解析。
c := colly.NewCollector(colly.ParseHTTPErrorResponse())
c.OnResponse(func(resp *colly.Response) {
// 这里可以读取状态码和错误页返回的结构化内容。
log.Printf("received status=%d url=%s", resp.StatusCode, resp.Request.URL)
})
c.OnError(func(resp *colly.Response, err error) {
// 开启该选项后,单纯的 HTTP 状态异常不再自动走这里。
log.Printf("request failed status=%d err=%v", resp.StatusCode, err)
})
开启后不要误解为“所有非 2xx 都成功了”。它只是改变 Colly 的回调路径,让响应可以进入 OnResponse、HTML 或 XML 处理;业务层仍然要检查 StatusCode。例如,429 需要保留限流信息并延迟,404 可能应该记录为资源不存在,500 才可能进入有限次数的重试。

把重试和日志决策放在分类之后
在我的抓取任务里,重试是分类之后的动作,而不是 OnError 一进来就调用 Retry。网络超时、临时连接失败可以有条件重试;参数错误、禁止访问、明显的 404 不应靠重试解决。HTTP 429 是否重试,则要结合响应头和站点约定,不能只看数字。
c.OnError(func(resp *colly.Response, err error) {
category := classifyCollyError(resp, err)
// 记录状态码和分类,便于把一次重试与原始请求关联起来。
log.Printf("fetch category=%s status=%d err=%v", category, statusCode(resp), err)
// 只对明确的传输类暂时故障重试,避免 404 或参数错误形成重试风暴。
if category == "transport-timeout" || category == "transport-error" {
if resp != nil && resp.Request != nil {
_ = resp.Request.Retry()
}
}
})
func statusCode(resp *colly.Response) int {
// 用 0 表示没有可用的真实 HTTP 状态码。
if resp == nil {
return 0
}
return resp.StatusCode
}
示例只展示决策位置,没有把重试次数、退避和幂等性藏起来。正式任务还应在 Context 或外部计数器中限制次数,并记录第一次失败的原因;如果站点对同一 URL 的重试有频率限制,还要把退避时间和域名级并发一起纳入设计。
异步模式和回调错误别混在一起
启用 colly.Async() 后,Visit 返回成功更多地表示任务已经提交,不能用它判断远端请求最终成功。要等 c.Wait() 让异步请求完成,并把最终结果交给 OnResponse、OnError 和 OnScraped 等回调。
c := colly.NewCollector(colly.Async())
c.OnError(func(resp *colly.Response, err error) {
// 异步场景的最终错误在回调中处理,不依赖 Visit 的即时返回值。
log.Printf("async error status=%d err=%v", statusCode(resp), err)
})
if err := c.Visit(targetURL); err != nil {
// 这里主要处理任务提交阶段的错误,例如 URL 或访问规则不合法。
log.Printf("submit visit failed: %v", err)
}
// 等待已经提交的网络任务完成,再结束本轮采集。
c.Wait()
最后可以用下面的速查表收束判断:
| 观察到的信号 | 优先归类 | 处理建议 |
|---|---|---|
Response 存在且状态码为 404/429/500 | HTTP 错误 | 保留状态码;按业务决定记录、限流等待或有限重试 |
Response 为空,error 能展开为 *url.Error | 传输错误 | 继续检查超时、取消、连接拒绝等原因 |
| 状态码为 2xx,但解析回调报错 | 处理阶段错误 | 单独统计解析问题,不算成网络断线 |
启用 ParseHTTPErrorResponse 后收到非 2xx | 可解析的 HTTP 响应 | 在响应回调里检查状态码,不依赖 OnError |
常见问题
OnError 的 Response 为 nil 就一定是网络错误吗?
不能绝对这样说。它只能说明当前没有可用的 HTTP 响应对象,仍需结合错误类型判断请求检查、取消或其他阶段错误。
为什么返回 404 时没有进入 OnResponse?
默认配置只按成功响应继续处理,非 2xx 会在错误处理路径中回调。若确实需要读取错误响应体,可以使用 colly.ParseHTTPErrorResponse(),但仍要在业务层检查状态码。
HTTP 500 是否应该自动 Retry?
不应该无条件自动重试。先确认接口是否幂等、服务端是否暂时故障、站点是否有频率限制,再设置次数和退避;4xx 通常更需要修正请求,而不是重复发送。
异步模式下如何知道所有请求结束?
调用 c.Wait() 等待已提交的异步任务,并在 OnError、OnResponse 等回调中收集最终结果。
Go Colly 同一个地址第二次 Visit 为什么被拒绝
- 上一篇
- Go Colly 同一个地址第二次 Visit 为什么被拒绝
- 下一篇
- Go GJSON 怎么只提取 JSON 数组中的指定字段
-
- Golang · Go问答 | 25分钟前 |
- Go Participle 增加前瞻和重写歧义语法有什么区别
- 426浏览 收藏
-
- Golang · Go问答 | 25分钟前 | Go问答 · Go 注释 ast 语法树 Participle Elide
- Go Participle 被 Elide 的注释为何不出现在语法树
- 198浏览 收藏
-
- Golang · Go问答 | 28分钟前 | go · Go 递归下降解析器 Participle 左递归
- Go Participle 左递归语法为什么无法直接解析
- 240浏览 收藏
-
- Golang · Go问答 | 35分钟前 |
- Go Goldmark 软换行与硬换行渲染结果有什么区别
- 161浏览 收藏
-
- Golang · Go问答 | 37分钟前 | Markdown · Go问答 · Go Goldmark parser.Context Markdown 转换 解析上下文 状态复用
- Go Goldmark 多次转换时为什么不能复用可变解析上下文
- 252浏览 收藏
-
- Golang · Go问答 | 47分钟前 |
- Go SJSON 原地替换选项为什么不能任意扩展字节长度
- 473浏览 收藏
-
- Golang · Go问答 | 50分钟前 |
- Go SJSON 数组末尾追加元素时路径应该怎么写
- 408浏览 收藏
-
- Golang · Go问答 | 59分钟前 |
- Go GJSON 数组条件中的单个匹配与全部匹配有什么区别
- 242浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 460次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 521次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 539次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 488次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 307次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览
-
- Go strings.ToValidUTF8 清洗日志内容的边界
- 2026-10-03 501浏览
-
- Go tls.GetCertificate 为什么收不到空 ServerName 请求
- 2026-09-27 501浏览

