問題
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
包。
http.Get()
![](https://imgkr2.cn-bj.ufileos.com/94738734-9402-475a-b41b-cb443f431f2f.html?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=Ceu9w6I4hvRLxVykLhh8IMwBbZ4%253D&Expires=1605828258)
-- 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
有可能退出的場景: -
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
, 這就是爲什麼代碼明明不規範但卻看不到明顯內存泄漏的原因。 -
那麼問題又來了,爲什麼上面要特意強調是同一個域名呢?改天,回頭,以後有空再說吧。
文章推薦:
-
連nil切片和空切片一不一樣都不清楚?那BAT面試官只好讓你回去等通知了。 -
昨天那個在for循環裏append元素的同事,今天還在麼? -
golang面試官:for select時,如果通道已經關閉會怎麼樣?如果只有一個case呢? -
golang面試官:for select時,如果通道已經關閉會怎麼樣?如果只有一個case呢? -
golang面試題:對已經關閉的的chan進行讀寫,會怎麼樣?爲什麼? -
golang面試題:對未初始化的的chan進行讀寫,會怎麼樣?爲什麼? -
golang 面試題:reflect(反射包)如何獲取字段 tag?爲什麼 json 包不能導出私有變量的 tag? -
golang面試題:json包變量不加tag會怎麼樣? -
golang面試題:怎麼避免內存逃逸? -
golang面試題:簡單聊聊內存逃逸? -
golang面試題:字符串轉成byte數組,會發生內存拷貝嗎? -
golang面試題:翻轉含有中文、數字、英文字母的字符串 -
golang面試題:拷貝大切片一定比小切片代價大嗎? -
golang面試題:能說說uintptr和unsafe.Pointer的區別嗎?
如果你想每天學習一個知識點?
本文分享自微信公衆號 - 編程三分鐘(coding3min)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。