## 問題
~~~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)來源:掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
- Golnag常見面試題目解析
- 交替打印數組和字母
- 判斷字符串中字符是否全都不同
- 翻轉字符串
- 判斷兩個給定的字符串排序后是否一致
- 字符串替換問題
- 機器人坐標計算
- 語法題目一
- 語法題目二
- goroutine和channel使用一
- 實現阻塞讀的并發安全Map
- 定時與 panic 恢復
- 高并發下的鎖與map讀寫問題
- 為 sync.WaitGroup 中Wait函數支持 WaitTimeout 功能.
- 七道語法找錯題目
- golang 并發題目測試
- 記一道字節跳動的算法面試題
- 多協程查詢切片問題
- 對已經關閉的的chan進行讀寫,會怎么樣?為什么?
- 簡單聊聊內存逃逸?
- 字符串轉成byte數組,會發生內存拷貝嗎?
- http包的內存泄漏