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客户端通…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...
云原生安全实战:API网关Kong的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关(API Gateway) API网关是微服务架构中的核心组件,负责统一管理所有API的流量入口。它像一座…...
Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
