Golang http请求忘记调用resp.Body.Close()而导致的协程泄漏问题(含面试常见协程泄漏相关测试题)

文章通过四个测试函数展示了在Go中进行HTTP请求时,是否调用`resp.Body.Close()`和`ioutil.ReadAll(resp.Body)`对goroutine数量的影响。不调用`resp.Body.Close()`会导致goroutine泄漏,而`ioutil.ReadAll(resp.Body)`会使连接进入空闲列表供复用。正确的做法是在请求后关闭Body以防止资源泄漏。
摘要由CSDN通过智能技术生成

参考:

先来看几道题,想一想最终的输出结果是多少呢?

package main

import (
	"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 读取完之后,会把当前的连接放入空闲列表中,供下次复用,所以算上主协程本身一共就 3 个协程。
  • testFun4() 这里执行了 5 次http请求,执行 resp.Body.Close(),也执行 ioutil.ReadAll(resp.Body)
    这里的结果和 testFun3() 一致,关键在于 执行了 ioutil.ReadAll(resp.Body),将 resp.body 读取完之后,会优先把当前的连接放入空闲列表中,供下次复用,即使你执行了 resp.Body.Close(),所以算上主协程本身一共就 3 个协程。

源码解析:

通过跟踪 go net/http 包的源码,得到其调用链路的流程图:

可以发现每次新建立一个http请求,最终底层实际上都会创建两个新的协程(写请求/读响应)

  • go pconn.readLoop():启动一个 读响应 相关的goroutine
  • go pconn.writeLoop():启动一个 写请求 相关的goroutine
    在这里插入图片描述

readLoop()writeLoop()本身都是for循环:

  • 只要【控制是否退出for循环的变量 alive】为true,for循环就会一直进行下去,且会把当前的连接放入空闲列表中,供下次复用
    • 示例:Fn正常的读body,当body读完之后,会向 waitForBodyRead推入一个true:waitForBodyRead <- isEOF
  • 而只有从 bodyEOF := <-waitForBodyRead中读出的值为false,for循环才会退出。
    示例:earlyCloseFn 未读取body就close的会执行此方法,可以发现向 waitForBodyRead推入一个false:waitForBodyRead <- false
    • 若退出for循环,最后会执行 readLoop 中的defer()函数。defer函数中的 pc.close(closeErr)不仅会关闭本身的通道closech,也会进而控制 writeLoop的退出。
      在这里插入图片描述

因此,waitForBodyRead这个chan对接下来goroutine的生死起着关键作用。

在这里插入图片描述

总结:

  • 日常开发中,在写代码时基本都不会遗漏 ioutil.ReadAll(resp.Body),但可能会存在忘记写 resp.Body.Close()的情况,这里可能会导致协程泄漏。
    但如果你请求的域名 url 不变的话,那么顶多只会泄漏一个负责 读响应 的goroutine 和一个负责 写请求 的goroutine,不会导致协程数飙升的情况,所以程序运行一般也不会出现什么太明显的问题。
  • 不过还是建议:在执行任何http 请求时,一定要记得加 resp.Body.Close(),避免异常情况下的goroutine泄漏,进而导致内存泄漏(每个goroutine初始时只占几kb,但量多了也扛不住),引起服务异常。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要将音频数据从原始格式转换为WAV格式,您需要使用一个音频处理库。其中一个流行的库是 `go-sox`,它使用SoX音频处理工具的接口。以下是一个将原始音频数据转换为WAV格式的示例代码: ```go package main import ( "io" "os" "os/exec" "github.com/krig/go-sox" ) func main() { inputFilename := "audio.raw" outputFilename := "audio.wav" // 读取原始音频数据 inputFile, err := os.Open(inputFilename) if err != nil { panic(err) } defer inputFile.Close() // 使用 io.ReadAll() 函数读取音频数据 audioData, err := io.ReadAll(inputFile) if err != nil { panic(err) } // 创建 SoX 对象 sox := sox.New() // 创建输入流 input := sox.OpenMem(audioData) // 创建输出流 output := sox.CreateFile(outputFilename, input.FileSignal()) // 配置输出流格式 outputSignal := sox.Signal{ Channels: input.Signal().Channels, Rate: input.Signal().Rate, Precision: 16, } output.SetSignal(outputSignal) // 创建转换链 transform := sox.CreateEffect(sox.FindEffect("input")) transform.AddInput(input) transform.AddOutput(output) // 运行转换 err = transform.Flow() if err != nil { panic(err) } // 关闭流 input.Release() output.Release() // 使用 ffmpeg 转换 WAV 格式 cmd := exec.Command("ffmpeg", "-i", outputFilename, "-acodec", "pcm_s16le", "-ar", "16000", outputFilename) err = cmd.Run() if err != nil { panic(err) } } ``` 这个示例代码使用了 `io.ReadAll()` 函数从文件中读取音频数据,然后使用 `go-sox` 库将其转换为WAV格式。最后,使用 `ffmpeg` 工具将输出文件转换为所需的PCM_S16LE格式。您需要确保在系统上安装了 `ffmpeg` 工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值