跳转至

Context

学习目标

学完本章后,学习者应该能够:

  1. 理解 context.Context 的设计目的。
  2. 正确使用取消、超时、截止时间和请求链路传递。
  3. 避免 context 使用误区。
  4. 能让 HTTP、数据库、Redis、RPC 调用遵守请求生命周期。

Context 解决什么问题

context 用来在调用链上传递:

  • 取消信号。
  • 超时时间。
  • 截止时间。
  • 请求作用域值。

它最重要的作用是控制生命周期,而不是当作万能参数包。

取消与超时

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

defer cancel() 很重要。即使超时还没发生,也应该释放相关定时器资源。

函数接收 context 时,通常作为第一个参数:

func GetUser(ctx context.Context, id int64) (User, error) {
    // ...
}

截止时间传递

入口请求有总超时,下游调用应该共享这个预算,而不是每一层重新设置更长超时。

func Handle(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
    defer cancel()

    _ = service.Do(ctx)
}

Value 的边界

context.WithValue 适合传递请求作用域元信息:

  • request id。
  • trace id。
  • auth principal。

不适合传递数据库连接、可选参数、大对象或业务配置。

Go 后端实际应用例子

例子一:HTTP 调用传递 context

func CallUserService(ctx context.Context, client *http.Client, userID int64) error {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://user/api/users/%d", userID), nil)
    if err != nil {
        return err
    }

    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}

上游请求取消后,下游 HTTP 请求也能尽快取消。

例子二:worker 响应取消

func Worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return
            }
            job.Handle(ctx)
        case <-ctx.Done():
            return
        }
    }
}

后台任务也应该有退出机制。

常见误区

  • 误区一:context 只是传 trace id。

context 的核心是取消和超时。Value 只是辅助能力。

  • 误区二:把 context 存进 struct。

context 应该随请求传递,不应该长期存储在 struct 中。

  • 误区三:函数内部随便创建 context.Background()

这样会切断上游取消信号。除非是进程级后台任务,否则应该接收上游 context。

线上问题案例

某 HTTP 接口客户端已经断开,但服务端继续执行数据库查询和下游 RPC,因为内部函数使用了 context.Background()。高峰期大量无意义工作堆积,拖慢服务。

修复方式是从 r.Context() 派生 context,并在所有数据库、Redis、HTTP、RPC 调用中传递。

实战任务

改造一个下游调用函数:

  1. 函数接收 context.Context
  2. HTTP 请求使用 NewRequestWithContext
  3. 设置总超时。
  4. 调用完成后关闭响应体。
  5. 说明为什么不能使用 context.Background()
参考答案

handler 层可以从 r.Context() 派生带超时的 context,然后传给 service 和 client。HTTP 请求使用 http.NewRequestWithContext,这样上游取消或超时时,请求可以被取消。

在请求链路中使用 context.Background() 会切断取消信号。客户端断开、入口超时或服务关闭时,下游调用仍可能继续运行,造成资源浪费和 goroutine 堆积。

面试题

1. context 主要解决什么问题?

参考答案

context 主要解决请求链路中的取消、超时、截止时间和请求作用域值传递问题。它让上游可以通知下游停止工作,避免无意义资源消耗。

在后端服务中,HTTP、数据库、Redis、RPC 都应该尽量接受并遵守 context。

2. 为什么要调用 cancel?

参考答案

context.WithCancelWithTimeoutWithDeadline 返回的 cancel 用于释放相关资源并通知子 context 取消。即使操作提前完成,也应该调用 cancel。

WithTimeout 来说,cancel 可以释放内部定时器资源。

3. context.Value 应该放什么?

参考答案

context.Value 适合放请求作用域的小型元数据,例如 request id、trace id、认证主体。它不应该放数据库连接、大对象、可选参数或业务配置。

业务参数应该通过显式函数参数传递,这样类型更清晰,也更容易测试。