一句话结论

Context 用于在 Goroutine 之间传递取消信号、截止时间和请求范围元数据。不是用来传普通业务参数的。取消是协作式的——Context 只发信号,不能强制终止 Goroutine。

核心原理

Context 接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)  // 截止时间
    Done() <-chan struct{}                     // 取消时关闭
    Err() error                                // 取消原因
    Value(key interface{}) interface{}         // 请求范围值
}

四大创建方法

// 1. 根 Context(永不取消)
ctx := context.Background()
ctx := context.TODO()  // 占位,不确定用什么 Context 时用

// 2. 可取消
ctx, cancel := context.WithCancel(parent)
cancel()  // 手动取消

// 3. 超时
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// 5 秒后自动取消

// 4. 截止时间
ctx, cancel := context.WithDeadline(parent, time.Now().Add(time.Minute))

取消传播树

Background()
   ├── WithCancel(parent) → ctx1 (cancel1)
   │   └── WithTimeout(parent) → ctx2
   │       └── WithValue(parent) → ctx3
   └── WithTimeout(parent) → ctx4

调用 cancel1() → ctx1, ctx2, ctx3 全部取消。ctx4 不受影响。
子 Context 取消,父 Context 不受影响。

Context 取消 ≠ 强制终止 Goroutine

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // Context 取消只是通知!Goroutine 能否退出取决于这里是否处理
            return
        default:
            // 耗时操作(比如调用不支持 Context 的库)
            doSomethingBlocking()  // 即使 Context 取消了,这也会继续执行
        }
    }
}

Context 取消只是关掉了 Done channel。Goroutine 需要主动检查 ctx.Done() 才能退出。

项目中的应用

Agent 任务取消(项目一-AI-Agent工作流与评测平台)

func executeAgentTask(ctx context.Context, task *Task) error {
    for _, step := range task.Steps {
        select {
        case <-ctx.Done():
            return ctx.Err()  // 用户取消了
        default:
        }
        result, err := callModelAPI(ctx, step)
        if err != nil {
            return err
        }
        saveResult(step, result)
    }
    return nil
}

gRPC 调用超时(项目二-物联网AI-Agent中枢控制平台)

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
status, err := deviceClient.GetStatus(ctx, &pb.DeviceRequest{Id: "pump-01"})
if err == context.DeadlineExceeded {
    // 超时降级:读 Redis 缓存
    return getCachedStatus("pump-01")
}

常见误用

误用

为什么不行

正确做法

把 Context 存进 struct

Context 是请求级别的,生命期不能超过请求

作为函数第一个参数传递

用 Context 传业务参数

Context.Value 无类型安全、无编译期检查

用函数参数或专用的 Request 结构体

忘记 cancel

WithTimeout 返回的 cancel 不调用 → 内存泄漏

总是 defer cancel()

传 nil Context

nil Context 的 Done() 返回 nil,select 会永久阻塞

不确定时用 context.TODO()

高频面试问题

Q: Context 为什么不能强杀 Goroutine?

30 秒回答: Go 没有提供强制终止 Goroutine 的 API——因为那会留下未释放的锁、未关闭的资源。Context 只是"通知"机制,Goroutine 自己检查 ctx.Done() 决定如何优雅退出。

Q: Context.Value 怎么用的?为啥不直接用全局变量?

30 秒回答: ctx.Value 存请求级别的元数据(如 trace_id、user_id),通过 context.WithValue 注入。比全局变量好的原因是:每个请求有独立的 Context 树,不同 Goroutine 不会互相污染。

Q: Context 与 errgroup 怎么结合?

30 秒回答: errgroup.WithContext 返回一个 Context——组内任何一个 Goroutine 返回 error,Context 自动取消,其他 Goroutine 收到取消信号后尽快退出。

最小实验

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

    go func() {
        select {
        case <-ctx.Done():
            fmt.Println("子 goroutine 收到取消:", ctx.Err())
        }
    }()

    time.Sleep(3 * time.Second)
    fmt.Println("done")
}
// 输出: 子 goroutine 收到取消: context deadline exceeded

速记

Context = 取消信号 + 截止时间 + 请求域 KV。WithCancel/WithTimeout/WithDeadline/WithValue 四大创建。取消树:子取消父不受影响。协作式取消,不能强杀。不存 struct,不传 nil,不传业务参数。errgroup 自动取消传播。