Golang http请求忘记调用resp.Body.Close()而导致的协程泄漏问题(含面试常见协程泄漏相关测试题)
参考:
- 知乎:别因为忘记close你的httpclient,造成goroutine泄漏
- CSDN:resp.Body.Close() 引发的内存泄漏goroutine个数
先来看几道题,想一想最终的输出结果是多少呢?
package mainimport ("fmt""io/ioutil""net/http""runtime"
)// 测试1:不执行resp.Body.Close(),也不执行ioutil.ReadAll(resp.Body)
func testFun1() {for i := 0; i < 5; i++ {_, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun1 http.Get err: ", err)return}}time.Sleep(time.Second * 1)fmt.Println("testFun1() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试2:执行resp.Body.Close(),不执行ioutil.ReadAll(resp.Body)
func testFun2() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun2 http.Get err: ", err)return}resp.Body.Close() // 执行resp.Body.Close()}// Close()过程需要一定时间,如果直接输出goroutine数量,可能出现连接还未完全回收的情况,结果有时为1有时为3// 因此,为了结果的准确性,我们这里休眠等待1秒,使得连接完全被回收。time.Sleep(time.Second * 1)fmt.Println("testFun2() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试3:不执行resp.Body.Close(),执行ioutil.ReadAll(resp.Body)
func testFun3() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun3 http.Get err: ", err)return}_, _ = ioutil.ReadAll(resp.Body) // 执行ioutil.ReadAll(resp.Body)}time.Sleep(time.Second * 1)fmt.Println("testFun3() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试4:执行resp.Body.Close(),也执行ioutil.ReadAll(resp.Body)
func testFun4() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun4 http.Get err: ", err)return}_, _ = ioutil.ReadAll(resp.Body) // 执行ioutil.ReadAll(resp.Body)resp.Body.Close() // 执行resp.Body.Close()}time.Sleep(time.Second * 1)fmt.Println("testFun4() 当前goroutine数量=", runtime.NumGoroutine())
}func main() {testFun1()testFun2()testFun3()testFun4()
}
答案:
- testFun1() 当前goroutine数量= 11
- testFun2() 当前goroutine数量= 1
- testFun3() 当前goroutine数量= 3
- testFun4() 当前goroutine数量= 3
注意:
针对以上testFun2(),如果仅仅执行了 resp.Body.Close()
,那么为了结果的准确性,我们在打印结果之前通过time.Sleep(time.Second * 1)
先休眠等待1秒,使得连接完全被回收后,然后再打印输出结果。
因为,Close()过程可能需要一定时间,如果直接输出goroutine数量,那么可能出现连接还未完全回收的情况,导致结果随机,有时为1有时为3 …
而针对以上testFun4(),虽然执行了 resp.Body.Close()
,但是同时也执行了 ioutil.ReadAll(resp.Body)
,这里会优先把当前的连接放入空闲列表中,供下次复用,所以不存在输出结果随机的情况。
解析:
首先要清楚,如果没有调用 resp.Body.Close()
,也就是没有 回收/释放 连接,那一定会存在协程的泄漏问题(进而导致内存泄漏)。
另外,稍微了解 go net/http 包的同学,都知道 每次执行http的 Get/Post 请求时,底层都会创建两个协程,分别处理 写请求/读响应 这两个事件。具体底层逻辑后面会提到 …
所以你可以简单的理解为:执行一条http请求时,go内部会创建两个协程。
接下来,针对每一个示例做分析:
- testFun1() 这里执行了 5 次http请求,且不执行
resp.Body.Close()
,也不执行ioutil.ReadAll(resp.Body)
:
因为 每次执行http的 Get/Post 请求时,底层都会创建两个协程,加上主协程本身,所以一共有 5*2 + 1 = 11 个协程。 - testFun2() 这里执行了 5 次http请求,执行
resp.Body.Close()
,不执行ioutil.ReadAll(resp.Body)
:
说明在执行resp.Body.Close()
后,回收了底层都会创建两个协程,只剩下主协程本身,所以一共就 1 个协程。 - testFun3() 这里执行了 5 次http请求,不执行
resp.Body.Close()
,执行ioutil.ReadAll(resp.Body)
:
当执行ioutil.ReadAll(resp.Body)
,将 resp.body 读取完之后,会把当前的连接放入空闲列表中,供下次复用。 - testFun4() 这里执行了 5 次http请求,执行
resp.Body.Close()
,也执行ioutil.ReadAll(resp.Body)
:
这里的结果和 testFun3() 一致,关键在于 执行了ioutil.ReadAll(resp.Body)
,将 resp.body 读取完之后,会优先把当前的连接放入空闲列表中,供下次复用,即使你执行了resp.Body.Close()
。
源码解析:
通过跟踪 go net/http 包的源码,得到其调用链路的流程图:
可以发现每次新建立一个http请求,最终底层实际上都会创建两个新的协程(写请求/读响应)
go pconn.readLoop()
:启动一个 读响应 相关的goroutinego pconn.writeLoop()
:启动一个 写请求 相关的goroutine
readLoop()
和 writeLoop()
本身都是for循环:
- 只要【控制是否退出循环的变量
alive
】为true,循环就会一直进行下去,且会把当前的连接放入空闲列表中,供下次复用。- 示例:Fn正常的读body,当body读完之后,会向
waitForBodyRead
推入一个true:waitForBodyRead <- isEOF
。
- 示例:Fn正常的读body,当body读完之后,会向
- 而只有从
bodyEOF := <-waitForBodyRead
中读出的值为false,循环才会退出。
示例:earlyCloseFn 未读取body就close的会执行此方法,可以发现向waitForBodyRead
推入一个false:waitForBodyRead <- false
。- 若退出循环,最后会执行 readLoop 中的defer()函数。defer函数中的
pc.close(closeErr)
不仅会关闭本身的通道closech,也会进而控制writeLoop
的退出。
- 若退出循环,最后会执行 readLoop 中的defer()函数。defer函数中的
因此,waitForBodyRead
这个chan对接下来goroutine的生死起着关键作用。
总结:
- 日常开发中,在写代码时基本都不会遗漏
ioutil.ReadAll(resp.Body)
,但可能会存在忘记写resp.Body.Close()
的情况,这里可能会导致协程泄漏。
但如果你请求的域名 url 不变的话,那么顶多只会泄漏一个负责 读响应 的goroutine 和一个负责 写请求 的goroutine,不会导致协程数飙升的情况,所以程序运行一般也不会出现什么太明显的问题。 - 不过还是建议:在执行任何http 请求时,一定要记得加
resp.Body.Close()
,避免异常情况下的goroutine泄漏,进而导致内存泄漏(每个goroutine初始时只占几kb,但量多了也扛不住),引起服务异常。
相关文章:

Golang http请求忘记调用resp.Body.Close()而导致的协程泄漏问题(含面试常见协程泄漏相关测试题)
参考: 知乎:别因为忘记close你的httpclient,造成goroutine泄漏 CSDN:resp.Body.Close() 引发的内存泄漏goroutine个数 先来看几道题,想一想最终的输出结果是多少呢? package mainimport ("fmt"…...

进程信号生命周期详解
信号和信号量半毛钱关系都没有! 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2 查看信号的机制,如默认处理动作man 7 signal SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理…...

2023-03-03干活小计
今天见识了 归一化的重要性:归一化 不容易爆炸 深度了解了学习率:其实很多操作 最后的结果都是改变了lr 以房价预测为例:一个点一个点更新 比较 矩阵的更新: 为什么小批量梯度下降 优于随机梯度下降 优于批量梯度下降ÿ…...
操作系统结构
随着操作系统的不断增多和代码规模的不断扩大,提供合理的结构对提升操作系统的安全与可靠性来说变得尤为重要。 1.分层法 指将操作系统分为若干层,最低层位硬件,最高层为用户接口,每层只能调用紧邻它的低层的功能和服务(类似于计…...

[SSD科普] 固态硬盘物理接口SATA、M.2、PCIe常见疑问,如何选择?
前言犹记得当年Windows 7系统体验指数中,那5.9分磁盘分数,在其余四项的7.9分面前,似乎已经告诉我们机械硬盘注定被时代淘汰。势如破竹的SSD固态硬盘,彻底打破了温彻斯特结构的机械硬盘多年来在电脑硬件领域的统治。SSD数倍于HDD机…...

【Java学习笔记】3.Java 基础语法
Java 基础语法 一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。 对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象ÿ…...
Python基础学习6——if语句
基本概念 if语句为条件判断语句,用来判断if后面的语句是真是假。if的用途有很多,比如作为条件测试可以判断两数是否相等与不等、进行数值笔记等等。例子如下: Lego_price (599, 799, 898) if Lego_price[0] 599:print("Correct!&quo…...

有免费的PDF转Word吗?值得收藏的7个免费 PDF转Word工具请收好
PDF 和 DOC 是人们在工作中广泛使用的两种最流行的文档格式。PDF 是 Adobe 的便携式文档格式,DOC 是 Microsoft 的 Word 文档格式。PDF 是一种更安全可靠的文件格式,因为它很难编辑 PDF 文件,但是有一些称为 PDF 编辑器的工具可用于编辑 PDF …...

Thinkphp6使用RabbitMQ消息队列
Thinkphp6连接使用RabbitMQ(不止tp6,其他框架对应改下也一样),如何使用Docker部署RabbitMQ,在上一篇已经讲了->传送门<-。 部署环境 开始前先进入RabbitMQ的web管理界面,选择Queues菜单,点…...

小成本互联网创业怎么做?低成本创业的方法分享
多数人都会有想法创业,尤其是在互联网上面创业,很多人看到了商机,但是因为成本的原因又放弃了,实际上,小成本也可以互联网创业!那么,小成本互联网创业怎么做?低成本创业的方法在这里…...
六、栈、栈的相关问题
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一、栈 1.栈概述 2.栈的实现 2.1 栈的API 2.2 栈的实现 二、栈的括号匹配问题 1.问题描述 2.代码实现 三、逆波兰表达式求值问题 1.问题描述 2.代码 总结 前言 提…...
Java安全停止线程
Thread 类虽提供了一个 stop() 方法(已经被废弃),但由于 stop() 方法强制终止一个正在执行的线程,可能会造成数据不一致的问题,所以在生产环境中最好不要使用。 场景: 由于一些操作需要轮询处理ÿ…...

12 readdir 函数
前言 在之前 ls 命令 中我们可以看到, ls 命令的执行也是依赖于 opendir, readdir, stat, lstat 等相关操作系统提供的相关系统调用来处理业务 因此 我们这里来进一步看一下 更细节的这些 系统调用 我们这里关注的是 readdir 这个函数, 入口系统调用是 getdents 如下调试…...

Windows环境搭建Android开发环境-Android Studio/Git/JDK
Windows环境搭建Android开发环境-Android Studio/Git/JDK 因为休假回来后公司的开发环境由Ubuntu变为了Windows,所以需要重新配置一下开发环境。 工作多年第一次使用Windows环境进行开发工作,作次记录下来。 一、 Git安装 1.1git 标题软件下载 网址&…...

全国爱耳日丨听力受损严重有哪些解决办法
——【科学爱耳护耳,实现主动健康】随着数码电子设备使用越来越方便、日常使用时间越来越长,听力障碍、患上耳道疾病一系列问题也接踵而至,在当下我们必须重视听力健康,采取更科学的听音方式,保护听力健康,…...

【抽水蓄能电站】基于粒子群优化算法的抽水蓄能电站的最佳调度方案研究(Matlab代码实现)
👨🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…...

【异常】因多租户字段缺少导致Error updating database. Column ‘tenant_id‘ cannot be null
一、报错内容 org.springframework.dao.DataIntegrityViolationException: ### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column tenant_id cannot be null ### The error may exist in com/xxx/cloud/mall/admin/mapper/Goods…...

类和对象(上)
文章目录 面向对象的初步认知类的实例化this引用对象的构造及初始化封装static成员代码块内部类 对象的打印一、面向对象的初步认知 Java是一门纯面向对象的语言(Object Oriented Program,简称OOP),在面向对象的世界里,一切皆为对象。在java中…...

Java经典面试题——谈谈 Java 反射机制,动态代理是基于什么原理?
典型回答 反射机制是 Java 语言提供的一种基本功能,赋予程序在运行时 自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法ÿ…...

19 客户端服务订阅机制的核心流程
Nacos客户端服务订阅机制的核心流程 说起Nacos的服务订阅机制,大家会觉得比较难理解,那我们就来详细分析一下,那我们先从Nacos订阅的概述说起 Nacos订阅概述 Nacos的订阅机制,如果用一句话来描述就是:Nacos客户端通…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...

图表类系列各种样式PPT模版分享
图标图表系列PPT模版,柱状图PPT模版,线状图PPT模版,折线图PPT模版,饼状图PPT模版,雷达图PPT模版,树状图PPT模版 图表类系列各种样式PPT模版分享:图表系列PPT模板https://pan.quark.cn/s/20d40aa…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...