多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
## 问题 ~~~go package main import ( "fmt" "io/ioutil" "net/http" "runtime" ) func main() { num := 6 for index := 0; index < num; index++ { resp, _ := http.Get("https://www.baidu.com") _, _ = ioutil.ReadAll(resp.Body) } fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine()) ~~~ } 上面这道题在不执行`resp.Body.Close()`的情况下,泄漏了吗?如果泄漏,泄漏了多少个 goroutine? ## 怎么答 不进行 resp.Body.Close(),泄漏是一定的。但是泄漏的 goroutine 个数就让我迷糊了。由于执行了 6 遍,每次泄漏一个读和写 goroutine,就是 12 个 goroutine,加上 main 函数本身也是一个 goroutine,所以答案是 13. 然而执行程序,发现答案是 3,出入有点大,为什么呢? ## 解释 我们直接看源码。golang 的 http 包。 ~~~go http.Get() -- DefaultClient.Get ----func (c *Client) do(req *Request) ------func send(ireq *Request, rt RoundTripper, deadline time.Time) -------- resp, didTimeout, err = send(req, c.transport(), deadline) // 以上代码在 go/1.12.7/libexec/src/net/http/client:174 func (c *Client) transport() RoundTripper { if c.Transport != nil { return c.Transport } return DefaultTransport } ~~~ * 说明`http.Get`默认使用`DefaultTransport`管理连接。 DefaultTransport 是干嘛的呢? ~~~go // It establishes network connections as needed // and caches them for reuse by subsequent calls. ~~~ * `DefaultTransport`的作用是根据需要建立网络连接并缓存它们以供后续调用重用。 那么`DefaultTransport`什么时候会建立连接呢? 接着上面的代码堆栈往下翻 ~~~go func send(ireq *Request, rt RoundTripper, deadline time.Time) --resp, err = rt.RoundTrip(req) // 以上代码在 go/1.12.7/libexec/src/net/http/client:250 func (t *Transport) RoundTrip(req *http.Request) func (t *Transport) roundTrip(req *Request) func (t *Transport) getConn(treq *transportRequest, cm connectMethod) func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) { ... go pconn.readLoop() // 启动一个读goroutine go pconn.writeLoop() // 启动一个写goroutine return pconn, nil } ~~~ * 一次建立连接,就会启动一个读 goroutine 和写 goroutine。这就是为什么一次`http.Get()`会泄漏两个 goroutine 的来源。 * 泄漏的来源知道了,也知道是因为没有执行 close **那为什么不执行 close 会泄漏呢?** 回到刚刚启动的读 goroutine 的`readLoop()`代码里 ~~~go func (pc *persistConn) readLoop() { alive := true for alive { ... // Before looping back to the top of this function and peeking on // the bufio.Reader, wait for the caller goroutine to finish // reading the response body. (or for cancelation or death) select { case bodyEOF := <-waitForBodyRead: pc.t.setReqCanceler(rc.req, nil) // before pc might return to idle pool alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && tryPutIdleConn(trace) if bodyEOF { eofc <- struct{}{} } case <-rc.req.Cancel: alive = false pc.t.CancelRequest(rc.req) case <-rc.req.Context().Done(): alive = false pc.t.cancelRequest(rc.req, rc.req.Context().Err()) case <-pc.closech: alive = false } ... } } ~~~ 其中第一个 body 被读取完或关闭这个 case: ~~~go alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && tryPutIdleConn(trace) ~~~ bodyEOF 来源于到一个通道 waitForBodyRead,这个字段的 true 和 false 直接决定了 alive 变量的值(alive=true 那读 goroutine 继续活着,循环,否则退出 goroutine)。 **那么这个通道的值是从哪里过来的呢?** ~~~go // go/1.12.7/libexec/src/net/http/transport.go: 1758 body := &bodyEOFSignal{ body: resp.Body, earlyCloseFn: func() error { waitForBodyRead <- false <-eofc // will be closed by deferred call at the end of the function return nil }, fn: func(err error) error { isEOF := err == io.EOF waitForBodyRead <- isEOF if isEOF { <-eofc // see comment above eofc declaration } else if err != nil { if cerr := pc.canceled(); cerr != nil { return cerr } } return err }, } ~~~ * 如果执行 earlyCloseFn ,waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出。 * 如果执行 fn ,其中包括正常情况下 body 读完数据抛出 io.EOF 时的 case,waitForBodyRead 通道输入的是 true,那 alive 会是 true,那么 readLoop() 这个 goroutine 就不会退出,同时还顺便执行了 tryPutIdleConn(trace) 。 ~~~go // tryPutIdleConn adds pconn to the list of idle persistent connections awaiting // a new request. // If pconn is no longer needed or not in a good state, tryPutIdleConn returns // an error explaining why it wasn't registered. // tryPutIdleConn does not close pconn. Use putOrCloseIdleConn instead for that. func (t *Transport) tryPutIdleConn(pconn *persistConn) error ~~~ * tryPutIdleConn 将 pconn 添加到等待新请求的空闲持久连接列表中,也就是之前说的连接会复用。 那么问题又来了,什么时候会执行这个`fn`和`earlyCloseFn`呢? ~~~go func (es *bodyEOFSignal) Close() error { es.mu.Lock() defer es.mu.Unlock() if es.closed { return nil } es.closed = true if es.earlyCloseFn != nil && es.rerr != io.EOF { return es.earlyCloseFn() // 关闭时执行 earlyCloseFn } err := es.body.Close() return es.condfn(err) } ~~~ * 上面这个其实就是我们比较收悉的 resp.Body.Close() ,在里面会执行 earlyCloseFn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出,goroutine 不会泄露。 ~~~go b, err = ioutil.ReadAll(resp.Body) --func ReadAll(r io.Reader) ----func readAll(r io.Reader, capacity int64) ------func (b *Buffer) ReadFrom(r io.Reader) // go/1.12.7/libexec/src/bytes/buffer.go:207 func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { for { ... m, e := r.Read(b.buf[i:cap(b.buf)]) // 看这里,是body在执行read方法 ... } } ~~~ * 这个`read`,其实就是`bodyEOFSignal`里的 ~~~go func (es *bodyEOFSignal) Read(p []byte) (n int, err error) { ... n, err = es.body.Read(p) if err != nil { ... // 这里会有一个io.EOF的报错,意思是读完了 err = es.condfn(err) } return } func (es *bodyEOFSignal) condfn(err error) error { if es.fn == nil { return err } err = es.fn(err) // 这了执行了 fn es.fn = nil return err } ~~~ * 上面这个其实就是我们比较收悉的读取 body 里的内容。 ioutil.ReadAll() ,在读完 body 的内容时会执行 fn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 true,alive 也会是 true,那 readLoop() 这个 goroutine 就不会退出,goroutine 会泄露,然后执行 tryPutIdleConn(trace) 把连接放回池子里复用。 ## 总结 * 所以结论呼之欲出了,虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了 ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读 goroutine 和一个写 goroutine,最后加上 main goroutine,所以答案就是 3 个 goroutine。 * 从另外一个角度说,正常情况下我们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 读 goroutine 和一个写 goroutine,这就是为什么代码明明不规范但却看不到明显内存泄漏的原因。 * 那么问题又来了,为什么上面要特意强调是同一个域名呢?改天,回头,以后有空再说吧。 > 作者:9 號同学 链接:[https://juejin.cn/post/6896993332019822605](https://juejin.cn/post/6896993332019822605)来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。