从一道题目开始, 如果你对这些输出不是很理解。接下我们将一步一步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
26func 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 | - http.Get |
1 | func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) { |
可以看到对于每一个链接都会启动一个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
62func (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
33func (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 | // 当earlyCloseFn != nil 和错误不等于io.EOF时会执行earlyCloseFn函数 |
1 | // 当readLoop()退出时,会close channel,writeLoop()接收到后便会退出 |
总结:
- 如果调用
ioutil.ReadAll
读取了resp.Body
,即使不调用resp.Body.Close()
也不会出现泄漏,为什么建议每次都要调用resp.Body.Close()
主要是因为处理异常的情况,当出现异常情况时,会往waitForBodyRead
发送一个false
使readLoop
退出。 - 如果没有读取连接中的数据也没有调用
resp.Body.Close()
,会导致readLoop
,writeLoop
不会退出从而产生泄漏。每次http.Get
都会产生两个goroutine
- 如果复用了连接将不会产生新的
readLoop
和writeLoop
goroutine
。