Go context 取消原因入门:让超时和主动取消更容易排查

本文讲解 context 取消、超时、取消原因和服务端请求链路中的传播方式,帮助初学者写出更容易排查的并发代码。

取消不只是停止,还要知道为什么停止

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 当成业务参数容器。取消原因应该短小、明确、面向排查。普通请求数据仍然应该通过函数参数传递。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页