教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang struct 字段null_给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...

golang struct 字段null_给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...

发布时间:2022-01-20   编辑:jiaochengji.com
教程集为您提供golang struct 字段null,给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...等资源,欢迎您收藏本站,我们将为您提供最新的golang struct 字段null,给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...资源

问题

<pre class="has"><code>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个数= %dn", runtime.NumGoroutine()) } </code></pre>

上面这道题在不执行<code>resp.Body.Close()</code>的情况下,泄漏了吗?如果泄漏,泄漏了多少个<code>goroutine</code>?

<h2>怎么答</h2> <ul><li>不进行<code>resp.Body.Close()</code>,泄漏是一定的。但是泄漏的<code>goroutine</code>个数就让我迷糊了。由于执行了6遍,每次泄漏一个读和写goroutine,就是12个goroutine,加上<code>main函数</code>本身也是一个<code>goroutine</code>,所以答案是13.</li><li>然而执行程序,发现答案是3,出入有点大,为什么呢?</li></ul><h2>解释</h2> <ul><li>我们直接看源码。<code>golang</code> 的 <code>http</code> 包。</li></ul>
<pre class="has"><code>http.Get() ![](https://imgkr2.cn-bj.ufileos.com/94738734-9402-475a-b41b-cb443f431f2f.html?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=Ceu9w6I4hvRLxVykLhh8IMwBbZ4%3D&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 } </code></pre>
<ul><li>说明 <code>http.Get</code> 默认使用 <code>DefaultTransport</code> 管理连接。</li></ul><h3><code>DefaultTransport</code> 是干嘛的呢?</h3>
<pre class="has"><code>// It establishes network connections as needed // and caches them for reuse by subsequent calls. </code></pre>
<ul><li><code>DefaultTransport</code> 的作用是根据需要建立网络连接并缓存它们以供后续调用重用。</li></ul><h3>那么 <code>DefaultTransport</code> 什么时候会建立连接呢?</h3>

接着上面的代码堆栈往下翻

<pre class="has"><code>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 } </code></pre>
<ul><li>一次建立连接,就会启动一个<code>读goroutine</code>和<code>写goroutine</code>。这就是为什么一次<code>http.Get()</code>会泄漏<code>两个goroutine</code>的来源。</li><li>泄漏的来源知道了,也知道是因为没有执行<code>close</code></li></ul><h3>那为什么不执行 <code>close</code> 会泄漏呢?</h3> <ul><li>回到刚刚启动的<code>读goroutine</code> 的 <code>readLoop()</code> 代码里</li></ul>
<pre class="has"><code>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 } ... } } </code></pre>
<ul><li>简单来说<code>readLoop</code>就是一个死循环,只要<code>alive</code>为<code>true</code>,<code>goroutine</code>就会一直存在</li><li><code>select</code> 里是 <code>goroutine</code> 有可能退出的场景: <ul><li><code>body</code> 被读取完毕或<code>body</code>关闭</li><li><code>request</code> 主动 <code>cancel</code></li><li><code>request</code> 的 <code>context Done</code> 状态 <code>true</code></li><li>当前的 <code>persistConn</code> 关闭</li></ul></li></ul>

其中第一个 <code>body</code> 被读取完或关闭这个 <code>case</code>:

<pre class="has"><code>alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && tryPutIdleConn(trace) </code></pre>

<code>bodyEOF</code> 来源于到一个通道 <code>waitForBodyRead</code>,这个字段的 <code>true</code> 和 <code>false</code> 直接决定了 <code>alive</code> 变量的值(<code>alive=true</code>那<code>读goroutine</code>继续活着,循环,否则退出<code>goroutine</code>)。

<h3>那么这个通道的值是从哪里过来的呢?</h3>
<pre class="has"><code>// 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 }, } </code></pre>
<ul><li>如果执行 <code>earlyCloseFn</code> ,<code>waitForBodyRead</code> 通道输入的是 <code>false</code>,<code>alive</code> 也会是 <code>false</code>,那 <code>readLoop()</code> 这个 <code>goroutine</code> 就会退出。</li><li>如果执行 <code>fn</code> ,其中包括正常情况下 <code>body</code> 读完数据抛出 <code>io.EOF</code> 时的 <code>case</code>,<code>waitForBodyRead</code> 通道输入的是 <code>true</code>,那 <code>alive</code> 会是 <code>true</code>,那么 <code>readLoop()</code> 这个 <code>goroutine</code> 就不会退出,同时还顺便执行了 <code>tryPutIdleConn(trace)</code> 。</li></ul>
<pre class="has"><code>// 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 </code></pre>
<ul><li><code>tryPutIdleConn</code> 将 <code>pconn</code> 添加到等待新请求的空闲持久连接列表中,也就是之前说的连接会复用。</li></ul><h3>那么问题又来了,什么时候会执行这个 <code>fn</code> 和 <code>earlyCloseFn</code> 呢?</h3>
<pre class="has"><code>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) } </code></pre>
<ul><li>上面这个其实就是我们比较收悉的 <code>resp.Body.Close()</code> ,在里面会执行 <code>earlyCloseFn</code>,也就是此时 <code>readLoop()</code> 里的 <code>waitForBodyRead</code> 通道输入的是 <code>false</code>,<code>alive</code> 也会是 <code>false</code>,那 <code>readLoop()</code> 这个 <code>goroutine</code> 就会退出,<code>goroutine</code> 不会泄露。</li></ul>
<pre class="has"><code>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方法 ... } } </code></pre>
<ul><li>这个<code>read</code>,其实就是 <code>bodyEOFSignal</code> 里的</li></ul>
<pre class="has"><code>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 } </code></pre>
<ul><li>上面这个其实就是我们比较收悉的读取 <code>body</code> 里的内容。 <code>ioutil.ReadAll()</code> ,在读完 <code>body</code> 的内容时会执行 <code>fn</code>,也就是此时 <code>readLoop()</code> 里的 <code>waitForBodyRead</code> 通道输入的是 <code>true</code>,<code>alive</code> 也会是 <code>true</code>,那 <code>readLoop()</code> 这个 <code>goroutine</code> 就不会退出,<code>goroutine</code> 会泄露,然后执行 <code>tryPutIdleConn(trace)</code> 把连接放回池子里复用。</li></ul><h2>总结</h2> <ul><li>所以结论呼之欲出了,虽然执行了 <code>6</code> 次循环,而且每次都没有执行 <code>Body.Close()</code> ,就是因为执行了<code>ioutil.ReadAll()</code>把内容都读出来了,连接得以复用,因此只泄漏了一个<code>读goroutine</code>和一个<code>写goroutine</code>,最后加上<code>main goroutine</code>,所以答案就是<code>3个goroutine</code>。</li><li>从另外一个角度说,正常情况下我们的代码都会执行 <code>ioutil.ReadAll()</code>,但如果此时忘了 <code>resp.Body.Close()</code>,确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 <code>读goroutine</code> 和一个<code>写goroutine</code>,这就是为什么代码明明不规范但却看不到明显内存泄漏的原因。</li><li>那么问题又来了,为什么上面要特意强调是同一个域名呢?改天,回头,以后有空再说吧。</li></ul><h2>文章推荐:</h2> <ul><li>昨天那个在for循环里append元素的同事,今天还在么?</li><li>对已经关闭的的 chan 进行读写,会怎么样?为什么?</li><li>对未初始化的的chan进行读写,会怎么样?为什么?</li><li>golang 面试题:reflect(反射包)如何获取字段 tag?为什么 json 包不能导出私有变量的 tag?</li><li>golang面试题:json包变量不加tag会怎么样?</li><li>golang面试题:怎么避免内存逃逸??</li><li>golang面试题:简单聊聊内存逃逸?</li><li>golang面试题:字符串转成byte数组,会发生内存拷贝吗?</li><li>golang面试题:翻转含有中文、数字、英文字母的字符串</li><li>golang面试题:拷贝大切片一定比小切片代价大吗?</li><li>golang面试题:能说说uintptr和unsafe.Pointer的区别吗?</li></ul><h3>如果你想每天学习一个知识点,关注我的【公】【众】【号】【golang小白成长记】。</h3>
到此这篇关于“golang struct 字段null_给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang struct 字段null_给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...
golang编程技巧:利用GC机制优雅地关闭协程,避免内存泄漏
了解 C 语言中的指针和内存泄漏及如何避免
golang 内存泄漏
golang 反射_golang 内存管理分析
golang垃圾回收
.go语言是否存在内存泄露问题?发现go语言内存泄漏的2种方法
使用pprof进行golang程序内存分析
golang 切片截取 内存泄露_怎么看待Goroutine 泄露
Golang垃圾回收机制

[关闭]
~ ~