
本文详解 go 程序批量请求 hacker news api 时出现 `eof` json 解析错误的根本原因(http 连接复用异常),并提供两种可靠解决方案:手动设置 `req.close = true` 或配置自定义 `http.transport` 禁用长连接。
在 Go 中批量请求多个 HTTP 接口(如 Hacker News 的 item API)时,若直接使用 http.Get() 循环调用,常会遇到看似随机的 EOF 错误,例如 json: EOF 或 unexpected end of JSON input。这并非数据本身损坏——单独访问对应 URL 完全正常,问题根源在于 服务端未按规范关闭 HTTP/1.1 持久连接。
Hacker News 的 API 服务器在返回响应后会主动关闭 TCP 连接,但未发送标准的 Connection: close 响应头。Go 的默认 HTTP 客户端(基于 http.DefaultClient)遵循 HTTP/1.1 协议,默认启用连接复用(keep-alive)。当它尝试复用一个已被服务端静默关闭的连接发起下一次请求时,底层读取将立即返回 EOF,导致 ioutil.ReadAll 获取空或截断的响应体,最终 JSON 解析失败。
✅ 正确解决方案
方案一:为每个请求显式关闭连接(推荐用于简单场景)
修改 get() 函数,不使用 http.Get(),而是构造 *http.Request 并设置 req.Close = true:
func get(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Close = true // 强制本次请求后关闭连接
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body) // 注意:ioutil 已弃用,使用 io.ReadAll
if err != nil {
return nil, err
}
return body, nil
}? 提示:ioutil 在 Go 1.16+ 中已弃用,请改用 io.ReadAll(需导入 "io" 包)。
方案二:全局禁用 Keep-Alive(适合高并发、统一管理场景)
创建自定义 http.Client,配置 Transport 禁用连接复用,避免逐个设置:
var httpClient = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true, // 关键:彻底禁用长连接
},
}
func get(url string) ([]byte, error) {
res, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}该方式更高效且线程安全,适用于需要频繁发起外部请求的服务。
⚠️ 注意事项与最佳实践
- 不要忽略 res.Body.Close():即使出错也应确保关闭响应体,防止文件描述符泄漏;
-
避免盲目追加原始字节:原代码中 contents = append(contents, body...) 将多个 JSON 对象的字节流拼接,结果不是合法 JSON 数组。建议解析为结构体后聚合:
type Item struct { ID int `json:"id"` Title string `json:"title"` } var items []Item for _, id := range ids[0:10] { body, _ := get(fmt.Sprintf("https://.../item/%d.json", id)) var item Item json.Unmarshal(body, &item) items = append(items, item) } - 考虑并发优化:上述循环是串行的。如需性能提升,可结合 sync.WaitGroup 或 errgroup 实现并发请求(注意控制并发数,避免触发服务端限流)。
通过理解 HTTP 连接生命周期与 Go 客户端行为的交互,并合理配置连接策略,即可稳定、高效地完成多 URL 请求任务。










