很多人把 c 直接传给数据库或下游客户端当 context 用,以为请求取消时它们会跟着取消。默认并不会。这篇验证 ContextWithFallback 关闭和打开时 Done、Err、Deadline、Value 的行为,以及它对客户端断开时子 context 的影响。
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() 也能得到同样的效果,代价是每个调用点都得记得这样写。
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 里有耗时的下游调用,这就是“客户端早走了,服务端还在白干”的来源之一。