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
,這就是爲何代碼明明不規範但卻看不到明顯內存泄漏的緣由。