request 设置超时
httpClient := http.Client{Timeout: 2 * time.Second}
if _, err := httpClient.Get("http://localhost:8090/timeout"); err != nil {
log.Fatal(err)
}
重试
httpClient.Get 流程
-
Client.do 调用 c.send(req, deadline)
-
send 调用 setRequestCancel, resp, err = rt.RoundTrip(req)
- setRequestCancel里面 req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
-
Transport.roundTrip 调用 pconn.roundTrip
...
ctx := req.Context()
...
for {
select {
case <-ctx.Done():
req.closeBody()
return nil, ctx.Err()
default:
}
...
// Get the cached or newly-created connection
pconn, err := t.getConn(treq, cm)
...
resp, err = pconn.roundTrip(treq)
-
Transport.getConn 调用 queueForIdleConn (Look for most recently-used idle connection.), 没有获取到再 queueForDial -> go t.dialConnFor(w)
-
Transport.dialConn
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
- persistConn.roundTrip
writeErrCh := make(chan error, 1)
pc.writech <- writeRequest{req, writeErrCh, continueCh}
resc := make(chan responseAndError)
pc.reqch <- requestAndChan{
req: req.Request,
cancelKey: req.cancelKey,
ch: resc,
addedGzip: requestedGzip,
continueCh: continueCh,
callerGone: gone,
}
... // 省略其他代码
for {
...// 省略其他代码
select {
case re := <-resc:
return re.res, nil
}
}
- persistConn.readLoop
rc := <-pc.reqch // 获取 reqch
resp, err = pc.readResponse(rc, trace)// readResponse
waitForBodyRead := make(chan bool, 2)
body := &bodyEOFSignal{ //封装body
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
},
}
resp.Body = body // 设置 resp.Body
select {
case rc.ch <- responseAndError{res: resp}://把resp写到rc.ch里面
case <-rc.callerGone:
return
}
Transport, DefaultTransport
// Transport is an implementation of [RoundTripper] that supports HTTP, // HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT). // // By default, Transport caches connections for future re-use. // This may leave many open connections when accessing many hosts. // This behavior can be managed using [Transport.CloseIdleConnections] method // and the [Transport.MaxIdleConnsPerHost] and [Transport.DisableKeepAlives] fields. //
type Transport struct {
...
idleConn map[connectMethodKey][]*persistConn // most recently used at end
...
}
type connectMethodKey struct {
proxy, scheme, addr string
onlyH1 bool
}
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp.Body.Close()
It is the caller's responsibility to // close Body. The default HTTP client's Transport may not // reuse HTTP/1.x "keep-alive" TCP connections if the Body is // not read to completion and closed.
在readLoop中,只有bodyEOF为true才会回收conn
waitForBodyRead 只有在 bodyEOFSignal.earlyCloseFn, bodyEOFSignal.fn 的时候才会写入,在bodyEOFSignal.Close 和 在bodyEOFSignal.Read 到eof的时候才会调用
select {
case bodyEOF := <-waitForBodyRead:
replaced := pc.t.replaceReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
alive = alive &&
bodyEOF &&
!pc.sawEOF &&
pc.wroteRequest() &&
replaced && tryPutIdleConn(trace)
if bodyEOF {
eofc <- struct{}{}
}
不执行resp.Body.Close() 时 goroutine 泄露问题
func main() {
num := 6
for index := 0; index < num; index++ {
_, _ = http.Get("https://www.baidu.com") //只get时有 13 个 goroutine
//log.Println(reflect.TypeOf(resp.Body)) // *http.gzipReader
//_, _ = io.ReadAll(resp.Body) // 加上ReadAll 或者 close 就只有3了
//resp.Body.Close()
}
fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine())
}
http.Get 默认使用 DefaultTransport 管理连接。
It establishes network connections as needed and caches them for reuse by subsequent calls.
go pconn.readLoop() // 启动一个读goroutine
go pconn.writeLoop() // 启动一个写goroutine
一次建立连接,就会启动一个读goroutine和写goroutine。这就是为什么一次http.Get()会泄漏两个goroutine的来源。