参考:

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

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

注意:

resp.Body.Close()time.Sleep(time.Second * 1)
resp.Body.Close()ioutil.ReadAll(resp.Body)

解析:

resp.Body.Close()

另外,稍微了解 go net/http 包的同学,都知道 每次执行http的 Get/Post 请求时,底层都会创建两个协程,分别处理 写请求/读响应 这两个事件。具体底层逻辑后面会提到 …
所以你可以简单的理解为:执行一条http请求时,go内部会创建两个协程。

接下来,针对每一个示例做分析:

resp.Body.Close()ioutil.ReadAll(resp.Body)resp.Body.Close()ioutil.ReadAll(resp.Body)resp.Body.Close()resp.Body.Close()ioutil.ReadAll(resp.Body)ioutil.ReadAll(resp.Body)resp.Body.Close()ioutil.ReadAll(resp.Body)ioutil.ReadAll(resp.Body)resp.Body.Close()

源码解析:

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

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

go pconn.readLoop()go pconn.writeLoop()
readLoop()writeLoop()
alivewaitForBodyReadwaitForBodyRead <- isEOFbodyEOF := <-waitForBodyReadwaitForBodyReadwaitForBodyRead <- falsepc.close(closeErr)writeLoop
waitForBodyRead

总结:

ioutil.ReadAll(resp.Body)resp.Body.Close()resp.Body.Close()