golang http.Get详解

从一道题目开始, 如果你对这些输出不是很理解。接下我们将一步一步debug源码彻底搞懂这些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
for i := 0; i < 3; i++ {
resp,_:=http.Get("https://www.baidu.com")
_, _ = ioutil.ReadAll(resp.Body)
}

fmt.Printf("Num of Goroutines %d\n", runtime.NumGoroutine()) // 3,readLoop + writeLoop + main

}

func main() {
for i := 0; i < 3; i++ {
resp,_:=http.Get("https://www.baidu.com")
resp.Body.Close()
}

fmt.Printf("Num of Goroutines %d\n", runtime.NumGoroutine()) // 3,readLoop + writeLoop + main
}


func main() {
for i := 0; i < 3; i++ {
http.Get("https://www.baidu.com")
}
fmt.Printf("Num of Goroutines %d\n", runtime.NumGoroutine()) // 7, 3 readLoop + 3 writeLoop + main = 7
}

本次主要涉及到如下三个文件
net/http/client.go
net/http/request.go
net/http/transport.go

http.Client:该类型表示可以发起 HTTP 请求的 HTTP 客户端。它提供了 Get、Post 和 Do 等方法,用于使用不同的 HTTP 方法发起 HTTP 请求。它还可以配置自定义的 http.Transport 对象,以控制底层网络连接。

http.Request:该类型表示可以被 HTTP 客户端发送的 HTTP 请求。它提供了 Method、URL 和 Header 等字段,用于设置请求的 HTTP 方法、URL 和 HTTP 头部。它还提供了设置请求体的方法。

http.Transport:该类型表示 HTTP 客户端用于发起 HTTP 请求的传输层,它负责TCP连接的创建和维护。它提供了配置底层网络连接的选项,例如最大空闲连接数、最大空闲连接时长和连接超时。http.Client 类型使用 http.Transport 的实例来发起 HTTP 请求。

http.Get的调用链如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- http.Get
- DefaultClient.Get
- c.Do(req)
- c.do(req)
- c.send(req,...)
- send(...)
- rt.RoundTrip(req)
- t.roundTrip(req)
- t.getConn(req, ...)
- t.queueForDial(w)
- t.dialConnFor
- t.dialConn(w.ctx, w.cm)
- go pconn.readLoop()
- go pconn.writeLoop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
pconn = &persistConn{
t: t,
cacheKey: cm.key(),
reqch: make(chan requestAndChan, 1),
writech: make(chan writeRequest, 1),
closech: make(chan struct{}),
writeErrCh: make(chan error, 1),
writeLoopDone: make(chan struct{}),
}
...
...

go pconn.readLoop()
go pconn.writeLoop()
return pconn, nil
}

可以看到对于每一个链接都会启动一个goroutine从链接中读取数据和一个goroutine往链接中写数据。
问题来了这两个goroutine什么时候退出呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
func (pc *persistConn) readLoop() {
closeErr := errReadLoopExiting // default value, if not changed below
defer func() {
pc.close(closeErr)
pc.t.removeIdleConn(pc)
}()


alive := true
for alive {
waitForBodyRead := make(chan bool, 2)
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
},
}

...
...

// 当alive=false时,readLoop()会结束运行
select {
// 当waitForBodyRead值为false的时候程序退出
// 当执行earlyCloseFn时,才会给waitForBodyRead赋值为false
case bodyEOF := <-waitForBodyRead:
pc.t.setReqCanceler(rc.req, nil)
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
}

}
}

我们来看下什么会执行earlyCloseFn函数和fn函数
fn函数在调用ioutil.ReadAll(resp.Body)时会被执行,而earlyCloseFn会在调用resp.Body.Close()时执行earlyCoseFn()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func (es *bodyEOFSignal) Read(p []byte) (n int, err error) {
es.mu.Lock()
closed, rerr := es.closed, es.rerr
es.mu.Unlock()
if closed {
return 0, errReadOnClosedResBody
}
if rerr != nil {
return 0, rerr
}

n, err = es.body.Read(p)
if err != nil {
es.mu.Lock()
defer es.mu.Unlock()
if es.rerr == nil {
es.rerr = err
}
err = es.condfn(err)
}
return
}

// 执行fn函数,会往waitForBodyRead发送true值
// 此时readLoop()并不会退出,但会把这个连接重新放回到连接池中(只有当数据被正常的读取完)
func (es *bodyEOFSignal) condfn(err error) error {
if es.fn == nil {
return err
}
err = es.fn(err)
es.fn = nil
return err
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当earlyCloseFn != nil 和错误不等于io.EOF时会执行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()
}
err := es.body.Close()
return es.condfn(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 当readLoop()退出时,会close channel,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
}
}
}

总结:

  1. 如果调用ioutil.ReadAll读取了resp.Body,即使不调用resp.Body.Close()也不会出现泄漏,为什么建议每次都要调用resp.Body.Close()主要是因为处理异常的情况,当出现异常情况时,会往waitForBodyRead发送一个false使readLoop退出。
  2. 如果没有读取连接中的数据也没有调用resp.Body.Close(),会导致readLoop,writeLoop不会退出从而产生泄漏。每次http.Get都会产生两个goroutine
  3. 如果复用了连接将不会产生新的readLoopwriteLoop goroutine