5.5 Context:取消传播与超时控制


5.5 Context:取消传播与超时控制

本节摘要:Context 是贯穿调用链的取消信号网——一个接口、四种派生函数、一个 Done 通道。本节讲清它的接口构成、从请求根到子操作的树形派生、WithCancel 的手工取消、WithTimeout 的时限控制、WithValue 的请求级传值边界,以及"Context 应作为函数第一个参数"等生产惯例与常见反模式。

本节导读

阅读完本节,你应当能够:

  1. 说出 Context 接口的四个方法与各自用途;
  2. 画出一次请求的 Context 派生树;
  3. 用 WithCancel 与 WithTimeout 控制子操作的生命周期;
  4. 克制地使用 WithValue 传递请求级数据;
  5. 避免"忘调 cancel""存进结构体"等反模式。

一、它解决什么问题

一个 HTTP 请求进来,要查数据库、调两个下游、再写缓存——四个 goroutine 并发跑。客户端等不及断开了,这四个动作还该继续烧资源吗?显然不该,但"通知整棵调用树停下来"在没有 Context 的时代要靠层层传自定义的取消通道,每个库自己发明轮子。Context 把这件事标准化:一条随请求生、随请求灭的信号链,任何阻塞操作都能挂在上面被统一叫停。

二、接口构成:一个信号灯加一个取值袋

type Context interface { Deadline() (deadline time.Time, ok bool) // 截止时间 Done() <-chan struct{} // 关闭即取消的信号通道 Err() error // 取消原因 Value(key any) any // 请求级取值 }

核心是 Done——一个只收的通道,取消发生时被关闭。回忆第 5.3 节:通道关闭对所有接收方广播。这就是 Context 取消能"一呼百应"的机理:select 等业务通道的同时等 Done,Done 一关,全部循环同时收到退出信号。

三、树形派生:根与枝

Context 是树状的。树的根有三种来历:标准库的 Background(空白根,main 函数与初始化用)、TODO(还没想好放哪个,占位)、以及服务器框架自动为每个请求创建的请求根(携带客户端断开信号)。从任何节点可以派生子节点:

取消向下传播:父取消,所有子孙自动取消;反过来子取消不影响父。超时同理——父的截止时间早于子的,父先到期一样把子带走。这棵树的形状就是"这次请求的资源依赖图"。

四个派生函数:

函数 能力 必须跟的动作
WithCancel 手工取消 defer cancel 释放资源
WithTimeout 相对时限(多久之后) 同上
WithDeadline 绝对时刻(到几点) 同上
WithValue 挂一个键值 用自定义键类型,别用字符串

四、实战三连

取消一个慢调用

ctx, cancel := context.WithCancel(context.Background()) go worker(ctx) time.Sleep(100 * time.Millisecond) cancel() // worker里的Done通道关闭,退出

worker 内部就是第 5.3 节的标准骨架:select 里一个业务分支一个 Done 分支。

限时完成

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // 即使提前完成也要调,释放定时器资源 select { case res := <-query(ctx): fmt.Println(res) case <-ctx.Done(): fmt.Println("失败:", ctx.Err()) // 超时或被取消 }

ctx 的 Err 返回取消原因:是"超时了"还是"被上游取消",日志里要分清。

请求级传值:追踪 ID、登录用户身份这类"每次请求一份、随调用链流动"的数据。纪律三条:键用自定义类型不用原生字符串(防撞);只放请求范围数据,不放可选参数(那是函数参数的工作);不塞大对象。

五、惯例与反模式

惯例:Context 作为函数第一个参数,命名就叫 ctx;阻塞操作(数据库查询、HTTP 调用、channel 收发)一律接收 ctx 版本——标准库早已全面改造,SQL 查询、HTTP 请求、redis 客户端都有 ctx 参数形态。

反模式清单

  • 忘调 cancel。WithTimeout 与 WithCancel 返回的 cancel 不调用,定时器与上下文对象要等到父取消才释放——高频请求下持续泄漏。铁律:拿到 cancel 立刻 defer。
  • 存进结构体。Context 是"随调用流动"的,塞进结构体字段等于把它钉死成全局状态,函数签名里看不到依赖,测试与审查都失明。个别框架型代码例外,业务代码没有例外。
  • 拿来传业务参数。WithValue 里放"每页条数"之类配置,是接口腐蚀的开始。
  • 在 nil 的 Done 上空转。自实现 Context 接口时 Done 返回 nil 通道会造成永久阻塞——直接嵌入标准库提供的基础实现是正道。

⚠️ 常见坑:把 cancel 当"清理函数偶尔调一下"。cancel 是资源归还动作,不调用就是占着定时器不走,高并发下表现为内存缓涨——排查时先看这个。

💡 关键直觉:Context 是请求的"心跳监测"——挂在上面的一切操作共享同一条命;心跳停了(Done 关闭),全部活动同步收工,没有谁需要单独通知。

温故知新

  • 动机:标准化"整棵调用树一起停"的信号传播。
  • 接口四方法:Deadline、Done、Err、Value;Done 是通道关闭广播。
  • 树形派生:取消自上而下,父亡子灭、子亡父存。
  • 四函数:Cancel 手工、Timeout 相对、Deadline 绝对、Value 传值。
  • cancel 必调:defer 它,否则定时器泄漏。
  • 第一参数惯例:ctx 进签名,不进结构体。
  • Value 三纪律:自定义键、只放请求级数据、不塞参数。

下一章回到单线程世界的高级武器:反射、unsafe、测试与工具链。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U