必须调用resp.body.close(),否则tcp连接无法归还连接池,导致文件描述符耗尽;defer需在err检查后调用,避免nil panic;transport需配置maxidleconns、maxidleconnsperhost和idleconntimeout。

http.Get后不调用resp.Body.Close()一定会泄露连接
只要没关 resp.Body,哪怕请求成功、数据已读完,底层 TCP 连接就不会归还给连接池,持续堆积直到耗尽文件描述符或触发 maxIdleConns 限制。这不是“可能泄露”,而是确定性行为。
- 默认
http.DefaultClient的MaxIdleConnsPerHost是 2,意味着最多只缓存 2 个空闲连接 —— 第 3 次请求若前 2 个未关闭,就会新建连接,旧连接卡在 TIME_WAIT 状态 -
io.ReadAll(resp.Body)或json.NewDecoder(resp.Body).Decode()不会自动关闭流,它们只读,不释放资源 - 即使
err != nil,只要resp非 nil,resp.Body仍需关闭(例如 4xx/5xx 响应也有 body)
defer resp.Body.Close() 的位置必须严格在 err 检查之后
把 defer resp.Body.Close() 写在 if err != nil 判断之前是常见错误 —— 此时 resp 可能为 nil,运行时 panic:panic: runtime error: invalid memory address or nil pointer dereference。
- 正确顺序:先检查
err,确认resp非 nil 后再defer resp.Body.Close() - 如果函数生命周期长(比如后台轮询 goroutine),
defer会延迟到函数退出才执行,可能导致连接长期滞留;此时应显式调用resp.Body.Close()并立即丢弃resp - 注意:
resp.Body.Close()可被多次调用,幂等,但仅第一次真正释放资源
高并发下光靠Close还不够,Transport配置必须收紧
即使每个请求都正确关闭 Body,若 Transport 未设限,连接池仍可能无限膨胀,尤其在目标服务响应慢或不稳定时。
-
MaxIdleConns设为 0(默认值)等于不限制全局空闲连接数,极易打满系统 fd 限额 -
MaxIdleConnsPerHost默认仅 2,对单域名高频请求极不友好,建议按压测结果设为 10–50 - 必须设置
IdleConnTimeout(如90 * time.Second),否则空闲连接永不超时,长期占用端口 - 避免直接用
http.Get(),改用自定义*http.Client,确保 Transport 可控
如何快速验证是否还有连接泄露
别只看内存,连接泄露最先暴露在系统级指标上 —— netstat -an | grep :YOUR_PORT | wc -l 或 lsof -p PID | grep TCP | wc -l 持续上涨就是信号。
- 启用
http.DefaultTransport的调试日志(需重编译或 patch transport)不现实,更实用的是用 pprof 查/debug/pprof/goroutine?debug=2,搜readLoopgoroutine 数量是否随请求数线性增长 - 观察
net/http.Server的handler日志里是否有大量http: server closed idle connection,说明客户端没及时复用或释放 - 一个常被忽略的点:重定向(302)会创建多个
resp,只有最终响应的Body需要 Close ——http.Client默认自动处理重定向,你只需关最终那个
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











