参考:

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

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,但量多了也扛不住),引起服务异常。

更多推荐