「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,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泄漏,加了必定不會。