Go context 所代表的语义
前言
context 的职责是传递取消信号、截止时间和请求范围的数据,而不是替业务代码决定"取消之后该返回什么结果"。
也就是说,它本质上是一种协作式取消机制,而不是运行时层面的抢占式终止。
只是"广播信号"
官方语义里,context 主要做三件事:
- 传递取消信号
- 传递 deadline / timeout
- 传递 request-scoped value
当上游取消后,下游通过:
select {
case <-ctx.Done():
return ctx.Err()
default:
}
或者在调用支持 Context 的 API 时,让这些 API 提前结束。
比如:
http.NewRequestWithContextdb.QueryContextexec.CommandContext
重点是:cancel() 只负责广播取消信号,不负责自动回滚业务状态,不负责自动构造部分结果,也不负责定义 HTTP 响应语义。
核心性质
1. context 不是强杀
context 的核心机制,是通过关闭 Done() 通道向调用链下游传播取消信号。至于 goroutine 何时退出、以什么方式退出,取决于调用方是否主动检查 ctx.Done(),或所调用的 API 是否原生支持 Context。
它更接近:
上游通知下游:当前请求已经失效,后续工作应尽快停止。
而不是:
运行时直接中断 goroutine 的执行现场。
2. 怎么收工,由各层自己决定
对于同一类取消信号,不同业务逻辑可以采取不同的收尾策略:
- 查询类接口:直接返回错误
- 聚合类接口:返回已收集到的部分结 果
- 数据库事务:回滚事务
- 文件下载:记录进度,等待下次续传
这些行为都不由 context 决定,而是由业务层自行定义取消后的语义。
3. context 解耦了生命周期控制和业务返回值
同一个底层函数,只要遵守 Context 传播约定,就可以复用于不同场景:
- A 场景要"尽量给部分结果"
- B 场景要"一旦取消就整次作废"
context 只负责表达:当前调用的生命周期约束已经发生变化。
至于上层如何解释这个变化,是返回部分结果、丢弃结果、触发重试,还是转换成错误响应,属于上层策略。
边界
1. 不是所有函数都会"自己安排部分结果"
更准确地说:
函数可以选择返回部分结果,也可以选择直接返回错误。
Go 里更常见、也更稳妥的默认做法其实是:
- 发现
ctx.Done() - 尽快停止
- 返回
ctx.Err()或包装后的错误
"返回部分结果"通常属于显式的业务容错设计,不是 context 的默认语义。
2. 回滚也不是 context 自动做的
例如数据库事务回滚,真正执行回滚动作的是:
- 你自己写的
tx.Rollback() - 或数据库驱动、事务封装层在收到取消后执行的清理逻辑
所以更准确的表述应该是:
context 负责通知取消;资源释放、事务回滚、结果保留与否,由具体代码或具体库负责。
HTTP 后端场景
假设有一个搜索接口:
// import "context" / "errors" / "encoding/json" / "net/http" …
func SearchHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := SearchAll(ctx, r.URL.Query().Get("q"))
if err != nil {
// 不要把所有错误都映射成 504:取消 / 超时 / 业务错误语义不同
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, err.Error(), http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
// 客户端已断开时,有时甚至不必再写响应
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
这里的 r.Context() 常见会在以下场景被取消:
- 用户主动断开连接
- 到后端的连接被代理关闭(不一定等于“代理配置了超时”这一个原因)
- 上游已经不再等待这次响应
因此 SearchAll 内部应该继续向下传递 ctx:
func SearchAll(ctx context.Context, query string) ([]Item, error) {
items, err := fetchFromES(ctx, query)
if err != nil {
return nil, err
}
detail, err := fetchDetail(ctx, items)
if err != nil {
return nil, err
}
return merge(items, detail), nil
}
如果某一层正在等待外部 I/O,就应该通过支持 Context 的调用方式响应取消:
func fetchFromES(ctx context.Context, query string) ([]Item, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://search.internal?q="+query, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return decodeItems(resp.Body)
}
如果客户端断开:
r.Context()被取消SearchAll这条调用链上的下游感知到ctx.Done()- 正在进行的 HTTP / DB / RPC 请求如果支持
Context,也会尽快结束
这正是 context 的核心价值:将"当前请求已无继续执行必要"这一生命周期信号沿调用链持续传播。
取消信号如何向下走
sequenceDiagram
participant Client
participant Handler
participant SearchAll
participant ES as fetchFromES
Client->>Handler: 请求 / 随后断开
Handler->>SearchAll: ctx = r.Context()
SearchAll->>ES: 带同一 ctx 的 HTTP
Note over Handler,ES: ctx.Done() 关闭
ES-->>SearchAll: 尽快返回错误
SearchAll-->>Handler: 返回 ctx.Err() 或包装错误
总结
context是协作式取消机制,不是抢占式终止- 它负责传递取消信号、超时约束和请求范围数据
- 取消后的返回语义由业务层自行定义
- 回滚、清理、资源释放由具体代码或具体库负责
- 在 HTTP 后端中,应优先从
r.Context()开始向下传播 - 调用外部资源时,应优先使用支持
Context的 API
context 决定的是调用是否还应继续执行,但不决定取消后的业务收尾策略。