yafeiaa Blogs

Golang| golang中为什么resp.body要关闭

本文介绍在golang的标准库net/http中,为什么请求之后,要主动关闭resp.body、以及不关闭resp.body有什么影响。

问题描述

在golang中,我们使用http标准库发起一个http请求,一般会使用如下的方式:

resp, err := http.Get("http://www.baidu.com")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(body))

在上面的代码中,我们使用http.Get发起一个http请求,然后使用ioutil.ReadAll读取resp.Body的内容,最后关闭resp.Body。

在学习其他编程语言,如python,我们一般不需要进行类似的操作,这是为什么呢?

问题分析

阅读源码

要回答这个问题,我们首先要知道net/http包在做请求的时候,背后都做了哪些事情,所以要阅读net/http包的源码。

getConn:获取一个连接,或者重新创建连接 getConn

queueForDial: 把连接创建请求(后文简称w,一个经过初始化的wantConn结构体)放入到dialer的队列中等待被使用

一旦满足以下两种条件:

  • 没有限制MaxConnsPerHost
  • 还没有达到MaxConnsPerHost

queueForDial

就会通过dialConnFor函数为w创建一个真正的往目标地址的连接,dialConnFor函数创建连接调用了dialConn函数。

而dialConn函数是最关键的,在dialConn函数的最后,拉起了两个goroutine:

  • readloop
  • writeloop

我们这里分析下这两个goroutine的作用以及退出条件。

  1. readloop

通过调用pc.readResponse函数接收response。

我们省略掉一些不重要的代码,只看关键的部分: readloop内部有一个for循环,循环的退出条件是:alive == false,而alive赋值为false的时机是:

  • resp.Close为true
  • rc.req.Close为true
  • resp.StatusCode == 199(https://http.dev/199)
  • bodyWritable为true

其他三点我们先忽略,重点关注resp.Close条件。这就是问题的答案,如果不执行resp.Body.Close,那么resp.Close永远为false,所以readloop永远不会退出,就会在程序生命周期中永远保留一个goroutine和文件句柄。

readLoop

  1. writeloop

writeloop的作用是往目标地址写入请求数据,并且进行一些写入的错误处理。

它内部是一个阻塞式的for-select控制结构,退出的条件是:

  • pc.closech能够读到数据
  • 写入数据时出错

writeloop

所以,如果我们在使用http.Get发起请求时,没有执行resp.Body.Close,那么readloop和writeloop就会一直保留,直到程序退出。

问题总结

在net/http包中,如果我们在发起请求时没有执行resp.Body.Close,那么就会导致readloop和writeloop这两个goroutine一直保留,直到程序退出。所以,为了防止goroutine泄露,我们需要在发起请求后,及时执行resp.Body.Close。

参考资料