闽公网安备 35020302035485号

// 来自 src/context/example_test.go
func ExampleWithValue() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
// Output:
// found value: Go
// key not found: color
}
取消// 来自 src/context/example_test.go
gen: = func(ctx context.Context) < -chan int {
dst: = make(chan int)
n: = 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst < -n:
n++
}
}
}()
return dst
}
然后通过 context.WithCancel 生成一个可取消的 Context,传入 gen 方法,直到 gen 返回 5 时,调用 cancel 取消 gen 方法的执行。// 来自 src/context/example_test.go
ctx, cancel: = context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n: = range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
// Output:
// 1
// 2
// 3
// 4
// 5
这么看起来,可以简单理解为在一个协程的循环中埋入结束标志,另一个协程去设置这个结束标志。// 来自 src/context/example_test.go
func ExampleWithTimeout() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel: = context.WithTimeout(context.Background(), shortDuration)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
// Output:
// context deadline exceeded
}
client: = http.Client {
Timeout: 10 * time.Second,
}
// 来自 src/net/http/client.go
type Client struct {
// ... 省略其他字段
// Timeout specifies a time limit for requests made by this
// Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
//
// A Timeout of zero means no timeout.
//
// The Client cancels requests to the underlying Transport
// as if the Request's Context ended.
//
// For compatibility, the Client will also use the deprecated
// CancelRequest method on Transport if found. New
// RoundTripper implementations should use the Request's Context
// for cancellation instead of implementing CancelRequest.
Timeout time.Duration
}
翻译一下注释:Timeout 包括了连接、redirect、读取数据的时间,定时器会在 Timeout 时间后打断数据的读取,设为0则没有超时限制。以一个最简单的例子来阐述超时机制的底层原理。这里我起了一个本地服务,用 Go HttpClient 去请求,超时时间设置为 10 分钟,建议使 Debug 时设置长一点,否则可能超时导致无法走完全流程.
client: = http.Client {
Timeout: 10 * time.Minute,
}
resp, err: = client.Get("http://127.0.0.1:81/hello")
1. 根据 timeout 计算出超时的时间点// 来自 src/net/http/client.go deadline = c.deadline()2. 设置请求的 cancel
// 来自 src/net/http/client.go stopTimer, didTimeout: = setRequestCancel(req, rt, deadline)设置的主要代码其实就是将请求的 Context 替换为 cancelCtx,后续所有的操作都将携带这个 cancelCtx:
// 来自 src/net/http/client.go
var cancelCtx func()
if oldCtx: = req.Context();
timeBeforeContextDeadline(deadline, oldCtx) {
req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
}
同时,再起一个定时器,当超时时间到了之后,将 timedOut 设置为 true,再调用 doCancel(),doCancel() 是调用真正 RoundTripper (代表一个 HTTP 请求事务)的 CancelRequest,也就是取消请求,这个跟实现有关。// 来自 src/net/http/client.go
timer: = time.NewTimer(time.Until(deadline))
var timedOut atomicBool
go func() {
select {
case <-initialReqCancel:
doCancel()
timer.Stop()
case <-timer.C:
timedOut.setTrue()
doCancel()
case <-stopTimerCh:
timer.Stop()
}
}()
Go 默认 RoundTripper CancelRequest 实现是关闭这个连接// 位于 src/net/http/transport.go
// CancelRequest cancels an in-flight request by closing its connection.
// CancelRequest should only be called after RoundTrip has returned.
func(t * Transport) CancelRequest(req * Request) {
t.cancelRequest(cancelKey {
req
}, errRequestCanceled)
}
3. 获取连接// 位于 src/net/http/transport.go
for {
select {
case <-ctx.Done():
req.closeBody()
return nil, ctx.Err()
default:
}
// ...
pconn, err: = t.getConn(treq, cm)
// ...
}
代码的开头监听 ctx.Done,如果超时则直接返回,使用 for 循环主要是为了请求的重试。// 位于 src/net/http/transport.go
// Queue for idle connection.
if delivered: = t.queueForIdleConn(w);
delivered {
// ...
return pc, nil
}
如果没有空闲连接,起个协程去异步建立,建立成功再通知主协程// 位于 src/net/http/transport.go // Queue for permission to dial. t.queueForDial(w)再接着是一个 select 等待连接建立成功、超时或者主动取消,这就实现了在连接过程中的超时
// 位于 src/net/http/transport.go
// Wait for completion or cancellation.
select {
case <-w.ready:
// ...
return w.pc, w.err
case <-req.Cancel:
return nil, errRequestCanceledConn
case <-req.Context().Done():
return nil, req.Context().Err()
case err:
= < -cancelc:
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
}
4. 读写数据// 位于 src/net/http/transport.go
go pconn.readLoop()
go pconn.writeLoop()
其中 wirteLoop 监听来自主协程的数据, 并往连接中写入
// 位于 src/net/http/transport.go
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 err != nil {
pc.close(err)
return
}
case <-pc.closech:
return
}
}
}
同理,readLoop 读取响应数据,并写回主协程。读与写的过程中如果超时了,连接将被关闭,报错退出。