bigywater.tech
Gin · 25

ContextWithFallback 与 c.Done()

很多人把 c 直接传给数据库或下游客户端当 context 用,以为请求取消时它们会跟着取消。默认并不会。这篇验证 ContextWithFallback 关闭和打开时 Done、Err、Deadline、Value 的行为,以及它对客户端断开时子 context 的影响。

400 ms
默认配置下 work(c) 没有被请求超时取消,自己跑满的时间
1502 ms vs 202 ms
客户端 200 ms 断开后,以 c 为父的子 context 结束的时间(关闭 / 打开 fallback)
T-1
只有打开 fallback 后 c.Value 才能读到 Request.Context 里的 trace 值

默认什么都不转发

实测 · 本机执行输出
  context.go(Gin v1.12.0):
    1433 | // hasRequestContext returns whether c.Request has Context and fallback.
    1434 | func (c *Context) hasRequestContext() bool {
    1435 |     hasFallback := c.engine != nil && c.engine.ContextWithFallback
    1436 |     hasRequestContext := c.Request != nil && c.Request.Context() != nil
    1437 |     return hasFallback && hasRequestContext
    1438 | }
    1439 | 
    1440 | // Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
    1441 | func (c *Context) Deadline() (deadline time.Time, ok bool) {
    1442 |     if !c.hasRequestContext() {
    1443 |         return
    1444 |     }
    1445 |     return c.Request.Context().Deadline()
    1446 | }
    1447 | 
    1448 | // Done returns nil (chan which will wait forever) when c.Request has no Context.
    1449 | func (c *Context) Done() <-chan struct{} {
    1450 |     if !c.hasRequestContext() {
    1451 |         return nil
    1452 |     }
    1453 |     return c.Request.Context().Done()
    1454 | }
    1455 | 
    1456 | // Err returns nil when c.Request has no Context.
    1457 | func (c *Context) Err() error {
    1458 |     if !c.hasRequestContext() {
    1459 |         return nil
    1460 |     }
    1461 |     return c.Request.Context().Err()
    1462 | }
    1463 | 
    1464 | // Value returns the value associated with this context for key, or nil
    1465 | // if no value is associated with key. Successive calls to Value with
    1466 | // the same key returns the same result.
    1467 | func (c *Context) Value(key any) any {
    1468 |     if key == ContextRequestKey {
    1469 |         return c.Request
    1470 |     }
    1471 |     if key == ContextKey {
    1472 |         return c
    1473 |     }
    1474 |     if keyAsString, ok := key.(string); ok {
    1475 |         if val, exists := c.Get(keyAsString); exists {
    1476 |             return val
    1477 |         }
    1478 |     }
    1479 |     if !c.hasRequestContext() {
    1480 |         return nil
    1481 |     }
    1482 |     return c.Request.Context().Value(key)
    1483 | }

*gin.Context 实现了 context.Context 接口,所以能直接传给接受 context 的函数,但默认的 Done() 是 nil、Err() 是 nil、没有 deadline。也就是说,它对下游来说是一个永远不会取消的 context;c.Value 只能读到 c.Keys 里的值,读不到 Request.Context() 里的。

打开之后

实测 · 本机执行输出
    ContextWithFallback=false:
      c.Done()==nil:true  c.Err()=<nil>  c.Deadline() ok=false
      c.Value("user")=alice  c.Value(ctxKey("trace"))=<nil>  c.Request.Context().Value(ctxKey("trace"))=T-1
      work(c):没有被取消,自己跑完 (400 ms)
      work(c.Request.Context()):context deadline exceeded 取消(101 ms)
    ContextWithFallback=true:
      c.Done()==nil:false  c.Err()=<nil>  c.Deadline() ok=true
      c.Value("user")=alice  c.Value(ctxKey("trace"))=T-1  c.Request.Context().Value(ctxKey("trace"))=T-1
      work(c):context deadline exceeded 取消(102 ms)
      work(c.Request.Context()):context deadline exceeded 取消(101 ms)

engine.ContextWithFallback = true 后,四个方法都转发给 c.Request.Context():中间件加的超时会让 work(c) 在 100 ms 左右被取消,trace 值也能读到。不想打开这个开关时,每次传 c.Request.Context() 也能得到同样的效果,代价是每个调用点都得记得这样写。

子 context 与客户端断开

实测 · 本机执行输出
    ContextWithFallback=false 客户端 200ms 时断开 → 子 context 结束:context deadline exceeded,1502 ms 后
    ContextWithFallback=true  客户端 200ms 时断开 → 子 context 结束:context canceled,202 ms 后

以 c 为父创建子 context,打开 fallback 时,客户端断开会通过 Request.Context() 的取消传下来(202 ms);关闭时子 context 对断开一无所知,只能等自己的超时(1502 ms)。如果 handler 里有耗时的下游调用,这就是“客户端早走了,服务端还在白干”的来源之一。

分步演示

ContextWithFallback
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。第二节用真实的 httptest.NewServer 与 http.Client;第三节用原始 TCP 连接发 GET 并在 200 ms 时关闭;每种情形各一次
    • 没有验证:fallback 打开时 Value 的查找开销;用 c 作 context 传给真实数据库驱动或 HTTP 客户端的行为(只用了自写的等待函数和 context.WithTimeout);ContextWithFallback 默认为 false 的历史原因(没有查证)
    京ICP备2026067502号-1京公网安备11010502063487号