给大家争脸了,用了三年golang,我还是没答对这道内存透露题。
问题
package mainimport ( "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()-- 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:250func (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:207func (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的区别吗?