如果我不关闭response.Body会发生什么?

159
在Go中,我有一些HTTP响应,并且有时会忘记调用:
resp.Body.Close()

在这种情况下会发生什么?会有内存泄漏吗?同时,在获取响应对象后立即放置defer resp.Body.Close()是否安全?

client := http.DefaultClient
resp, err := client.Do(req)
defer resp.Body.Close()
if err != nil {
    return nil, err
}

如果出现错误,respresp.Body 是否会为 nil?


当存在返回时,将defer resp.Body.Close()放在err != nil之后是可以的,因为当err不为nil时,它已经被关闭了。另一方面,当请求成功时,需要显式地关闭body。 - Vasantha Ganesh
5个回答

190

在这种情况下会发生什么?会有内存泄漏吗?

这是资源泄漏。连接不会被重用,可能会保持打开状态,在这种情况下文件描述符将不会被释放。

同时,在获取响应对象后立即使用defer resp.Body.Close()是安全的吗?

不是的,请遵循文档中提供的示例,并在检查错误后立即关闭。

client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
    return nil, err
}
defer resp.Body.Close()

来自http.Client文档:

如果返回的错误为nil,则响应将包含一个非nil的Body,用户需要关闭它。如果Body既没有被读到EOF也没有被关闭,客户端的底层RoundTripper(通常是Transport)可能无法重用持久TCP连接以进行后续的“保持活动”请求。


6
根据这个链接,你的代码仍然可能会造成连接泄漏。有一些情况下,响应不是nil而且错误也不是nil。 - mmcdole
32
那篇帖子是错误的,而且没有保证不会出现 panic 错误,因为在错误时返回的任何响应都没有定义的状态。如果一个 Body 在出现错误时没有关闭,则这是一个 bug,需要报告。你应该遵循官方客户端文档的说明,即“在出现错误时,可以忽略任何 Response”,而不是随机的博客文章。 - JimB
2
@del-boy:如果你预计客户端会发出更多请求,那么你应该尝试读取正文,以便可以重用连接。如果你不需要连接,那么就不要费心读取正文。如果你读取了正文,请使用io.LimitReader进行包装。我通常使用相当小的限制,因为如果请求太大,建立新连接会更快。 - JimB
4
值得注意的是,执行 _, err := client.Do(req) 也会导致文件描述符保持打开状态。因此,即使不关心响应内容,仍然需要将其赋值给变量并关闭 body。 - j boschiero
2
对于任何感兴趣的人,完整的文档如下(重点是):“在错误情况下,可以忽略任何响应。只有当CheckRedirect失败时,才会出现具有非nil错误的非nil响应,即使在这种情况下,返回的Response.Body已经关闭。” - nishanthshanmugham
显示剩余19条评论

17

如果Response.Body没有被Close()方法关闭,则与fd相关联的资源将不会被释放。这是一种资源泄漏。

关闭Response.Body

根据响应源代码

关闭Body是调用方的责任。

因此,该对象没有绑定最终器(finalizers),必须显式地关闭。

错误处理和延迟清理

在出现错误时,可以忽略任何响应。只有在CheckRedirect失败时才会出现非空的带错误的响应,即使在这种情况下,返回的Response.Body也已经关闭。

resp, err := http.Get("http://example.com/")
if err != nil {
    // Handle error if error is non-nil
}
defer resp.Body.Close() // Close body only if response non-nil

5
请注意,他们应该在错误处理条件内返回。如果用户没有在错误处理中返回,这将导致恐慌。 - applewood

6

首先,与上述提到的内容一样,描述符永远不会关闭。

此外,如果 DisableKeepAlives 为 false,则 Golang 会缓存连接(使用 persistConn 结构进行封装)以便重用。

在 Golang 中,在使用 client.Do 方法后,go 将作为步骤之一运行 goroutine readLoop 方法。

因此,在 golang http transport.go 中,只有当 req 在 readLoop 方法中取消时,pconn(persistConn struct) 才不会被放入 idleConn 通道中,并且此 goroutine(readLoop 方法) 也将被阻塞直到 req 被取消。

这里是代码 显示它。

如果您想了解更多,请查看 readLoop 方法。


0

请参见https://golang.org/src/net/http/client.go
“当err为nil时,resp始终包含一个非nil的resp.Body。”

但是他们没有说当err!= nil时,resp始终为nil。他们接着说:
“如果未关闭resp.Body,则客户端的基础RoundTripper(通常为Transport)可能无法重用持久的TCP连接以进行后续的“keep-alive”请求。”

因此,我通常会像这样解决问题:

client := http.DefaultClient
resp, err := client.Do(req)
if resp != nil {
   defer resp.Body.Close()
}
if err != nil {
    return nil, err 
}

4
这是不正确的,而且无法保证在出现错误时resp.Body不为nil。 - JimB
1
感谢@JimB。文档中的措辞是“在出现错误时,可以忽略任何响应。”更准确的说法是“在出现错误时,响应正文始终会被关闭。” - candita
3
不行,因为通常没有需要关闭的响应体。如果您继续阅读文档中的该段落:“仅当CheckRedirect失败时具有非nil错误的非nil响应才会出现,并且即使如此也已经关闭了返回的Response.Body。” - JimB

0
一个选项是将后续请求放入新的上下文中,这样您就可以使用相同的变量名称(如果您想要),而不必担心覆盖任何现有变量,并仍然可以关闭所有内容:
package main

import (
   "bytes"
   "net/http"
)

func main() {
   b := new(bytes.Buffer)
   // request one
   r, e := http.Get("http://speedtest.atl.hivelocity.net")
   if e != nil {
      panic(e)
   }
   defer r.Body.Close()
   b.ReadFrom(r.Body)
   // request two
   {
      r, e := http.Get("http://speedtest.lax.hivelocity.net")
      if e != nil {
         panic(e)
      }
      defer r.Body.Close()
      b.ReadFrom(r.Body)
   }
   // print
   print(b.String())
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接