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
?golang
resp.Body.Close()
,泄漏是必定的。可是泄漏的goroutine
个数就让我迷糊了。因为执行了6遍,每次泄漏一个读和写goroutine,就是12个goroutine,加上main函数
自己也是一个goroutine
,因此答案是13.golang
的 http
包。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
是干吗的呢?// It establishes network connections as needed // and caches them for reuse by subsequent calls.
DefaultTransport
的做用是根据须要创建网络链接并缓存它们以供后续调用重用。DefaultTransport
何时会创建链接呢?接着上面的代码堆栈往下翻面试
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()
代码里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 } ... } }
readLoop
就是一个死循环,只要alive
为true
,goroutine
就会一直存在select
里是 goroutine
有可能退出的场景:json
body
被读取完毕或body
关闭request
主动 cancel
request
的 context Done
状态 true
persistConn
关闭其中第一个 body
被读取完或关闭这个 case
:数组
alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && tryPutIdleConn(trace)
bodyEOF
来源于到一个通道 waitForBodyRead
,这个字段的 true
和 false
直接决定了 alive
变量的值(alive=true
那读goroutine
继续活着,循环,不然退出goroutine
)。缓存
// 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)
。// 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
呢?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
不会泄露。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
里的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
,这就是为何代码明明不规范但却看不到明显内存泄漏的缘由。