如何使用context取消goroutine_Go协程退出方案说明

context.WithCancel 是最常用且可控的 goroutine 退出方式,它通过协作式取消机制让协程主动监听 ctx.Done() 并安全退出,需配合 select 持续响应、避免轮询、正确处理 err 且 cancel 仅调用一次。

context.WithCancel 是最常用且可控的 goroutine 退出方式

Go 中没有直接“杀掉” goroutine 的机制,context.WithCancel 提供了一种协作式取消信号传递机制。它不强制终止协程,而是让协程主动检查 ctx.Done() 并自行退出,这是唯一被 Go 官方推荐、安全且可预测的方式。

常见错误是只调用 cancel() 却没在 goroutine 内监听 ctx.Done(),结果协程继续运行甚至泄漏;或者监听了但没处理 ctx.Err() 导致逻辑卡死。

  • 必须在 goroutine 启动时传入 ctx,不能事后注入
  • 监听需用 select 配合 ctx.Done(),不能用 if ctx.Err() != nil 轮询(浪费 CPU)
  • cancel() 只能调用一次,重复调用会 panic;建议用 defer cancel() 配合作用域管理

goroutine 中正确监听 context.Done() 的写法

监听不是“检查一次就完事”,而是要在可能阻塞或长期运行的路径中持续响应取消信号。典型场景包括:HTTP 请求、channel 接收、定时器等待、数据库查询等。

错误示例是把 select 放在循环外,导致只检查一次;或漏掉对 case 分支的清理逻辑(如关闭 channel、释放资源)。

func worker(ctx context.Context, jobs <-chan int) {
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return
            }
            process(job)
        case <-ctx.Done():
            // 必须在此处做清理:关闭下游 channel、释放锁、记录日志等
            log.Println("worker exit due to:", ctx.Err())
            return
        }
    }
}

不要用 time.After 或 time.Sleep 替代 context 超时控制

time.After(5 * time.Second) 看似简单,但它会创建一个不可取消的 timer,即使父 context 已取消,timer 仍会触发,造成 goroutine 意外唤醒或资源滞留。真正需要的是与 context 生命周期绑定的超时行为。

正确做法是用 context.WithTimeoutcontext.WithDeadline,它们返回的 ctx 在超时后自动关闭 Done() channel,且可被上层统一取消。

  • context.WithTimeout(parent, 5*time.Second) → 基于相对时间,适合大多数场景
  • context.WithDeadline(parent, time.Now().Add(5*time.Second)) → 基于绝对时间,适合跨系统协调
  • 永远不要在 select 中混用 time.Afterctx.Done(),除非你明确知道 timer 不会泄露

子 goroutine 必须继承并传播 context,不能用 background 或 todo 替代

启动子协程时若传入 context.Background()context.TODO(),等于切断了取消链路。上级调用 cancel() 后,子 goroutine 完全收不到信号。

正确做法是将上游传入的 ctx 显式传给每个子 goroutine,并在必要时用 context.WithValue 附加请求级数据(注意:仅限只读元信息,不要传业务对象)。

func handleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    // 正确:子 goroutine 继承并传播 ctx
    go processAsync(ctx, r.Body)
// 错误:断开 context 链路
// go processAsync(context.Background(), r.Body)

}

func processAsync(ctx context.Context, body io.ReadCloser) { defer body.Close() select { case

最容易被忽略的是:context 取消后,goroutine 退出前的清理工作是否完整。比如未关闭的 channel、未释放的 mutex、未关闭的文件句柄,这些不会因为 ctx.Done() 触发而自动回收。每次写 case ,都要问一句——这里该关什么、该记什么、该通知谁。