专栏首页golang小白成长记给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。

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

问题

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,出入有点大,为什么呢?

解释

  • 我们直接看源码。golanghttp 包。
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 会泄漏呢?
  • 回到刚刚启动的读goroutinereadLoop() 代码里
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就是一个死循环,只要alivetruegoroutine就会一直存在
  • select 里面是 goroutine 有可能退出的场景:
    • body 被读取完毕或body关闭
    • request 主动 cancel
    • requestcontext Done 状态 true
    • 当前的 persistConn 关闭

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

alive = alive &&
    bodyEOF &&
    !pc.sawEOF &&
    pc.wroteRequest() &&
    tryPutIdleConn(trace)

bodyEOF 来源于到一个通道 waitForBodyRead,这个字段的 truefalse 直接决定了 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
   },
  }
  • 如果执行 earlyCloseFnwaitForBodyRead 通道输入的是 falsealive 也会是 false,那 readLoop() 这个 goroutine 就会退出。
  • 如果执行 fn ,其中包括正常情况下 body 读完数据抛出 io.EOF 时的 casewaitForBodyRead 通道输入的是 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
  • tryPutIdleConnpconn 添加到等待新请求的空闲持久连接列表中,也就是之前说的连接会复用。
那么问题又来了,什么时候会执行这个 fnearlyCloseFn 呢?
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 通道输入的是 falsealive 也会是 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 通道输入的是 truealive 也会是 true,那 readLoop() 这个 goroutine 就不会退出,goroutine 会泄露,然后执行 tryPutIdleConn(trace) 把连接放回池子里复用。

总结

  • 所以结论呼之欲出了,虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读goroutine和一个写goroutine,最后加上main goroutine,所以答案就是3个goroutine
  • 从另外一个角度说,正常情况下我们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 读goroutine 和一个写goroutine这就是为什么代码明明不规范但却看不到明显内存泄漏的原因
  • 那么问题又来了,为什么上面要特意强调是同一个域名呢?改天,回头,以后有空再说吧。

本文分享自微信公众号 - golang小白成长记(golangxbczj),作者:咏春警告的胖虎

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-11-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • PHP 不会死 —— 我们如何使用 Golang 来阻止 PHP 走向衰亡

    在过去的十年中,无论是世界 500 强企业或是仅拥有 500 名用户的企业,我的团队都曾为他们开发过软件。在此期间,我的工程团队主要使用 PHP 进行后端开发。...

    猿哥
  • 全程带阻:记一次授权网络攻防演练(上)

    完整攻击链大概包括信息搜集、漏洞利用、建立据点、权限提升、权限维持、横向移动、痕迹清除等七步,虽然这个站点只经历了前四步,但也具有较强的代表性,组合利用漏洞形成...

    FB客服
  • AK47所向披靡,内存泄漏一网打尽

    青囊,喜欢运动T恤加皮裤的非典型程序猿。此时,他正目不转睛注视着屏幕上一行行的代码,内存泄漏这个问题已经让他茶饭不思两三天了,任凭偌大的雨滴捶打着窗户也无动于衷...

    233333
  • 动图图解!既然IP层会分片,为什么TCP层也还要分段?

    在网络层(IP层),叫分片。(注意以下提到的IP没有特殊说明的情况下,都是指IPV4)

    9号同学
  • 在“天眼”看到弑母案嫌疑人之前,我们付出了什么?

    不久前,一则新闻引起巨大震动,三年前弑母案中的嫌疑人吴谢宇在重庆江北机场被抓,有人透露吴谢宇进入机场不到十分钟,警察便找到了他。这场所谓的“完美犯罪”是否完美我...

    FB客服
  • 新技术加速隐私暴露,如何应对?(二)

    网络爬虫技术并不是一个新技术,最初是搜索引擎用来抓取散落在因特网上的海量网站,解析后为用户提供搜索服务的一种技术,本质上是模拟浏览器浏览网页的行为,用程序获取网...

    数据猿
  • 五一出游,“我”的隐私被扒干净了

    区块链大本营
  • golang 垃圾回收 gc

    摘要 在实际使用go语言的过程中,碰到了一些看似奇怪的内存占用现象,于是决定对go语言的垃圾回收模型进行一些研究。本文对研究的结果进行一下总结。 什么是垃圾回收...

    李海彬
  • ZAO 刷屏后 让我担心被随意拿走的隐私 还仅仅是个开始……

    一秒换脸 " 莱昂纳多 ",两秒占据 " 舞王 C 位 ",一夜之间暴走网络—— AI 换脸软件 ZAO 经过周末的集中式爆发,逐渐偃旗息鼓。

    鲸准商业评论
  • 全球4亿条用户电话号码曝光 Facebook再曝巨大安全漏洞

    9月5日据外媒消息报道,社交大佬平台Facebook存在严重的安全漏洞,一个存储了数以亿条与Facebook帐户关联的电话号码数据库在网上泄露,每条记录都包含一...

    新梦想IT职业教育
  • 【火绒安全周报】特朗普竞选网站遭黑客攻击 央视曝光 AI 黑产你的脸仅卖2分钱

    据报道,美国总统特朗普的竞选网站遭到短暂的黑客攻击,该网站的“About”页面被一个收集加密货币的信息取代。黑客声称自己掌握了“新冠病毒起源”的内部信息以及其他...

    用户6477171
  • 丢手机太危险了!

    今天看到一篇文章,看完后真的感觉丢手机太危险了,大家一定要注意,丢手机后第一时间挂失手机卡,银行卡。

    格姗知识圈
  • 美团超详细面经(附答案)

    Java3y
  • 盘点分析 | 2021第一季度国内外重大数据泄漏事件

    广东珠海有10万余条中小学生个人信息已经被非法泄露。近日,珠海网警在“净网2021”专项行动中破获一个侵犯公民个人信息的犯罪团伙,抓获6名嫌疑人,查获中小学生个...

    笑看
  • 起底身份倒卖产业:那些被公开叫卖的人生

    一个是潜逃近三年的北大高材生弑母案嫌疑人吴谢宇在重庆被捕,被抓时身上携带三十多张身份证,靠着不停变换身份,躲避着追捕。

    数据猿
  • 架构师写的BUG,非比寻常

    小伙话不多,但一旦说话斩钉截铁,带着无法撼动的自信。原因就是,有他着数亿高并发经验,每一秒钟的请求,都是其他企业运行一年也无法企及的。这就让人非常羡慕,毕竟他靠...

    xjjdog
  • 趣谈GC技术,解密垃圾回收的玄学理论(一)

    导语:大多数程序员在日常开发中常常会碰到GC的问题:OOM异常、GC停顿等,这些异常直接导致糟糕的用户体验,如果不能得到及时处理,还会严重影响应用程序的性能。本...

    腾讯云中间件团队
  • 分析 《7吨碳九泄漏,40万泉港人在无声中消逝》虚假报道

    看到标题,你想到什么?肯定是7吨碳9导致40万人死亡。但事实却并非如此,报道是假的。

    @坤的
  • 分析 《7吨碳九泄漏,40万泉港人在无声中消逝》虚假报道

    本着向来不看单一报道评判一个事情的原则,根据关键字查询了下国内外的近期新闻,有介绍的都是小众媒体和自媒体转发北风刘先生的文章。并没有看到有其他正式的媒体介绍相关...

    @坤的

扫码关注云+社区

领取腾讯云代金券