2026-10-31·by Sijie Wang

Context

Context:取消、超时、请求数据,一路往调用树下走

动机:done channel 跨层就不好使了。 done 解决了“怎么停”,但真实程序里取消信号要穿透很多层:HTTP handler、service、repo、DB driver 或者下游调用。每层传一个 done channel,签名全被污染,还带不了超时、带不了请求数据。context 就是把 done 标准化、升级之后的东西:一个对象沿着调用链往下传,取消、超时、请求级数据三件事打包在一起。

接口四个方法。 Done() 返回那个 done channel,关了就是停。Err() 说为什么停,是取消还是超时。Deadline() 说有没有超时点、到哪个时间点。Value(key) 带请求级数据,比如 request ID,跟着这次请求一路走。

派生是树,取消只往下走。 从 Background 出发,用 WithCancel、WithTimeout、WithDeadline、WithValue 派生子 context。父取消,所有子一起取消,跟关 done 广播一个道理,只是成树了。子取消不影响父和兄弟。值是不可变的,派生出来的是新对象,父本身永远不变。

第一个参数,绝不塞 struct。 三点。生命周期对不上:context 属于一次请求,struct 常常长期存在,塞进去等于把一次请求的取消信号绑到长期对象上,请求结束了信号还挂着,或者反过来拿着已取消的 context 继续用。取消链断了:显式传参让每个人都看得到取消路径,藏进字段里,接手的人就不知道这函数会不会被取消。签名即文档:第一个参数一看就知道这函数可取消、有超时。

WithCancel、WithTimeout 返回的 cancel 一定要调。 惯例是 defer cancel()。不调,子 context 就挂在父的 children 里,直到父取消或者超时才释放,这就是 context 泄漏。WithTimeout 就是 WithDeadline(现在加一段时间),到点自动取消。Err() 能分清原因:手动取消是 Canceled,超时是 DeadlineExceeded。

使用形态就两种。 循环里 select 等 ctx.Done(),一关就 return ctx.Err();阻塞调用回来后查 Err(),分清是正常错还是取消/超时。往下游传就把同一个 ctx 交给支持它的调用:http.NewRequestWithContext、数据库的 QueryContext,一个 ctx 走完整条链。

Value 的规矩。 key 用自定义非导出类型,防不同包撞 key。只放请求级元数据:request ID、trace ID、auth 信息。业务对象不放,更不拿它传本该写进参数的东西。

几个不要。 不传 nil,没 context 用 Background、占位用 TODO;cancel 不忘调;不存 struct;不用 Value 传业务数据或可选参数。

React 类比,以及它的边界。 React Context 解决 prop drilling,Go context 解决 done drilling:都是把要穿透多层的东西从中间每一层的签名里抽出来。两处差别。React Context 带的是 UI 状态,变了会触发重渲染;Go context 带的是控制信号和请求元数据,不是业务状态。React Context 是隐式的,哪里用 hook 哪里就能拿到;Go context 是显式的,必须当第一个参数传下去,沿着不可变的树单向流动。

生产注记。 web 服务里,每个请求从 HTTP 请求 context 派生自己的 context,超时设在层与层的边界,服务器关闭时取消在途请求的 context。context 就是把“客户端走了”变成“活儿真的停了”的东西。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →