「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」前端
http请求是咱们开发中最为常见的一个东西了,特别微服务中,因为服务的拆分,每一个人可能负责某一块的业务,当A服务的某个业务依赖B服务的数据时,最多见的就是B服务提供一个接口了。golang提供的原生的httpclient仍是很是强大的,可是若是在某些场景中,用的不对,可能会形成意想不到的问题。
先来看个问题:golang
func main() {
for i := 0; i < 100; i++ {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(resp.Body)
}
fmt.Println("goroutine num is", runtime.NumGoroutine())
}
复制代码
第一次接触go,我有如下几个疑问:后端
带着以上的几点疑问,我测试了几个例子:markdown
func main() {
httpWithoutClose()
}
func httpWithoutClose() {
for i := 0; i < 20; i++ {
_, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
}
fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
复制代码
直接发起20个请求,且不读response的body,也不进行response的body的close。
结果: goroutine num is 41
居然有41个goroutine,21个我却是能够理解(起码20个请求+1个主goroutine),41个说明每一个请求对应2个goroutine。经过阅读源码,发现大体请求的流程如图:app
获取链接以后,会新增两个goroutine,
readLoop
和 writeLoop
,这样就能够理解通了,一个负责读,一个负责写。函数
func httpWithClose() {
for i := 0; i < 20; i++ {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
resp.Body.Close()
}
fmt.Println("goroutine num is", runtime.NumGoroutine())
}
复制代码
直接发起20个请求,且不读response的body,可是进行response的body的close。
结果: goroutine num is 1
说明close以后,回收了readLoop
和 writeLoop
以readLoop中的一段代码为例:微服务
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
未读body就close的,会走此方法,能够发现向waitForBodyRead
推入一个false
Fn
正常的读body,当body读完以后,会向waitForBodyRead
推入一个true
。
waitForBodyRead
这个chan对接下来的goroutine的生死起着关键做用。
readLoop
自己是个循环:oop
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 cancellation or death)
select {
case bodyEOF := <-waitForBodyRead:
pc.t.setReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
alive = alive &&
bodyEOF && // false的话就退出循环
!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.cancelKey, rc.req.Context().Err())
case <-pc.closech:
alive = false
}
testHookReadLoopBeforeNextRead()
}
复制代码
只要alive=true
,循环就会一直进行下去,当从bodyEOF := <-waitForBodyRead
读出的是false,循环退出。而后 readLoop
执行defer
函数:post
defer func() {
pc.close(closeErr) // 关闭自身的通道 closech
pc.t.removeIdleConn(pc) // 回收链接
}()
复制代码
其中pc.close(closeErr)
,会关闭pc自己的通道closech,而后不是还有个writeLoop吗,writeLoop自己也是个循环,主要负责写的。测试
func (pc *persistConn) writeLoop() {
defer close(pc.writeLoopDone)
for {
select {
case wr := <-pc.writech:
startBytesWritten := pc.nwrite
err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
if bre, ok := err.(requestBodyReadError); ok {
err = bre.error
// Errors reading from the user's
// Request.Body are high priority.
// Set it here before sending on the
// channels below or calling
// pc.close() which tears town
// connections and causes other
// errors.
wr.req.setError(err)
}
if err == nil {
err = pc.bw.Flush()
}
if err != nil {
wr.req.Request.closeBody()
if pc.nwrite == startBytesWritten {
err = nothingWrittenError{err}
}
}
pc.writeErrCh <- err // to the body reader, which might recycle us
wr.ch <- err // to the roundTrip function
if err != nil {
pc.close(err)
return
}
case <-pc.closech: //收到消息后,退出
return
}
}
}
复制代码
当收到pc.closech信号的时候,writeLoop也就退出了。
因此只剩一个主goroutine了。
func httpWithoutCloseButRead() {
for i := 0; i < 20; i++ {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(resp.Body)
}
fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
复制代码
直接发起20个请求,读取body,但不close。
结果: goroutine num is 3
由上个例子咱们知道,当body读取完以后,会向waitForBodyRead
推入个true
,在true
的状况下,readLoop
会一直循环,且会把当前的链接放入空闲列表中,供下次使用:
select {
case bodyEOF := <-waitForBodyRead:
pc.t.setReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
alive = alive &&
bodyEOF &&
!pc.sawEOF &&
pc.wroteRequest() &&
tryPutIdleConn(trace) //放入idle list,供下次使用
if bodyEOF {
eofc <- struct{}{}
}
....
复制代码
由于没有回收readLoop
和 wirteLoop
两个goroutine,且下一个请求能够复用链接,因此就是3个
func main() {
httpCloseAndRead()
}
func httpCloseAndRead() {
for i := 0; i < 20; i++ {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
复制代码
和http 不带close,可是有read
的同样。
任何http request的时候,必定要加close
func doRequest() {
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close() // 很重要
_, err = ioutil.ReadAll(resp.Body)
}
复制代码
不加close,可能形成goroutine泄漏,加了必定不会。