取消不只是停止,还要知道为什么停止
Go 的 context.Context 常用于请求范围控制:客户端断开,请求超时,后台任务停止,外部调用取消。早期我们通常只能通过 ctx.Err() 知道是 context.Canceled 还是 context.DeadlineExceeded。这已经有用,但在复杂服务里,有时还想知道更具体的原因:是用户主动取消,还是权限检查失败后取消后续任务,还是上游熔断导致停止。
现代 Go 提供了带取消原因的 context API,可以在取消时附带一个错误原因,再由下游读取。它不应该被滥用成业务数据传递通道,但对排查并发任务为什么停止很有帮助。
这篇文章先复习普通 context,再讲取消原因如何使用。
普通取消和超时
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
<-ctx.Done()
fmt.Println("stopped:", ctx.Err())
}()
cancel()
超时:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
ctx.Err() 常见返回:
context canceled
context deadline exceeded
服务端 handler 中应该把请求 context 传下去:
user, err := service.GetUser(r.Context(), id)
这样客户端断开或请求超时时,数据库和 HTTP 客户端也有机会停止。
带原因的取消
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("quota exceeded"))
<-ctx.Done()
fmt.Println(ctx.Err())
fmt.Println(context.Cause(ctx))
ctx.Err() 仍然是通用错误,context.Cause(ctx) 能拿到具体原因。
一个后台任务示例:
func runJob(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return fmt.Errorf("job stopped: %w", context.Cause(ctx))
}
}
取消:
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
if err := runJob(ctx); err != nil {
log.Println(err)
}
}()
cancel(fmt.Errorf("service shutting down"))
日志会比单纯 context canceled 更有信息量。
超时也可以带原因
ctx, cancel := context.WithTimeoutCause(
context.Background(),
2*time.Second,
fmt.Errorf("call payment provider timeout"),
)
defer cancel()
下游:
select {
case <-ctx.Done():
return context.Cause(ctx)
case result := <-resultCh:
return result.Err
}
这适合把“哪个环节超时”记录清楚。但也要注意,原因应该是错误上下文,不要把大量业务数据塞进去。context 的职责仍然是取消、超时和请求范围值,不是通用参数包。
在并发任务中传播取消
假设多个 worker 处理任务,只要一个发现严重错误,就取消其他 worker:
func runWorkers(ctx context.Context, jobs []Job) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
var wg sync.WaitGroup
for _, job := range jobs {
job := job
wg.Add(1)
go func() {
defer wg.Done()
if err := processJob(ctx, job); err != nil {
cancel(fmt.Errorf("job %d failed: %w", job.ID, err))
}
}()
}
wg.Wait()
return context.Cause(ctx)
}
这个示例展示思路:第一个失败的任务可以取消整个任务组,并带上失败原因。真实项目里还要注意并发错误收集、重复 cancel 和返回 nil 的情况。
小结
context 的核心仍然是取消和超时传播。带原因的取消让你在排查时知道“为什么取消”,比只有 context canceled 更有上下文。常见用法是服务关闭、任务组失败、外部调用超时和主动中止后续工作。
不要把 context 当成业务参数容器。取消原因应该短小、明确、面向排查。普通请求数据仍然应该通过函数参数传递。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。