原文地址:manishrjain.com/must-close-… TIL: Go Response Body MUST be closed, even if you don’t read it
【翻译】每天一个知识点——Go中的http.Response. Body即使不读也必须关闭
我最近目睹了一次Go服务器中goroutine泄漏,现场看起来像这样:
goroutine profile: total 41053
20524 @ 0x43a656 0x44a7fe 0x65c512 0x46bec1
# 0x65c511 net/http.(*persistConn).writeLoop+0xf1 /usr/local/go/src/net/http/transport.go:2410
20523 @ 0x43a656 0x44a7fe 0x65b425 0x46bec1
# 0x65b424 net/http.(*persistConn).readLoop+0xd84 /usr/local/go/src/net/http/transport.go:2227
网上搜索了一下,绝大多数人都认为一定有未关闭的http.Response.Body
。 但我记得只有读了Body
才需要关闭,没有读就无需关闭。
因此,我一直在检查所有读Body
的地方,确保每个地方都在读取后关闭了Body
。
client := u.GetHTTPClient()
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "addCloudflareDNS")
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrapf(err, "addCloudflareDNS")
}
但偏偏忽视了这个看似再简单不过的健康检查代码。
resp, err := http.Get("https://url/health")
if err != nil {
return err
}
// MUST read resp.Body and do a resp.Body.Close() here
if resp.StatusCode == 200 {
return nil
}
return fmt.Errorf("invalid response status: %d", resp.StatusCode)
事实证明,无论是否需要(处理Response
),都必须始终读Body
并将其关闭。http.Response.Body
的注释中也提到了这一点:
The http Client and Transport guarantee that Body is always non-nil, even on responses without a body or responses with a zero-length body. It is the caller’s responsibility to close Body. The default HTTP client’s Transport may not reuse HTTP/1.x “keep-alive” TCP connections if the Body is not read to completion and closed.
不过,还是很有可能偶尔忘记关闭Body
。避免这个问题的一个简单方法是永远不要使用http.Get()
或任何使用了http.DefaultClient
的方法,而是仅使用自定义的客户端。
var httpClient *http.Client
func HTTPClient() *http.Client {
return httpClient
}
func init() {
// Initialize an HTTP client. We'll use this for every connection.
t := http.DefaultTransport.(*http.Transport).Clone()
t.MaxIdleConns = 100
t.MaxConnsPerHost = 100
t.MaxIdleConnsPerHost = 100
httpClient = &http.Client{
Timeout: time.Minute,
Transport: t,
}
}
如果改用这个 httpClient
,Go会为你关闭未关闭的响应体。 (为什么?——译者注)
Go的http.Response是反模式
我敢说Go的http.Response
是一种反模式,因为这里存在隐含的期望——即使调用者不需要处理Response
,也必须读取并关闭http.Response.Body
。 现在看来,这是一个糟糕的API。
糟糕的原因在于,对于需要清理的对象,典型模式是:
obj, err := GetObject(...)
if err != nil { check(err) }
defer obj.Close()
执行 obj.Close()
将使 obj
运行处理其内部状态所需的所有清理操作。如果这是一种模式,那么没有人会对http.Get()
或httpClient.Do()
等感到困惑。但这些方法的模式是:
obj, err := GetObject(...)
if err != nil { check(err) }
defer func() {
if obj.InnerField != nil {
cleanUp(obj.InnerField)
obj.InnerField.Close()
}
}
奇怪的地方在于每次获取obj
(相当于http.Response
)后,还需要关注InnerField
(相当于http.Response.Body
)以及如何清理它。如果还有另一个也需要清理(关闭)的InnerField2
呢?恐怕会出现更糟糕的情况:
obj, err := GetObject(...)
if err != nil { check(err) }
defer func() {
if obj.InnerField1 != nil {
cleanUp1(obj.InnerField1)
obj.InnerField1.Close()
}
if obj.InnerField2 != nil {
cleanUp2(obj.InnerField2)
obj.InnerField2.Close()
}
}()
所以,我敢打赌http.Response
实际上缺少一个 Close()
函数,该函数的缺失导致我们必须显式地读取并关闭 Response
中的内部字段Body
,最终产生了一种根深蒂固于最常用的库http
中的反模式。