Go: Go Context 上下文

最后更新:2026-08-26

Context 是 Go 并发控制的基石——它让超时、取消和传值在 goroutine 调用链中优雅传递。

当你的服务需要同时处理几十个外部依赖时(每个依赖都有自己的超时和取消逻辑),如何用统一的方式管理它们?这节课你将掌握 Go context 包的完整用法。

1. 你将学到


2. 一个后端工程师的真实故事

(1) 痛点:一个上游超时,整个系统雪崩

Xiaoli 是订单系统的工程师,她负责的 REST API dependency 3 个下游服务:

"下单接口调用了 3 个服务:库存服务、支付服务、通知服务。有一天库存服务卡了 20 秒才响应,结果我的服务内存暴涨,所有 goroutine 都在等它——其他用户的请求也进不来了。老板问'为什么下单页面全挂了?'"

问题分析:

GO
// 坏代码:没有超时控制
func PlaceOrder(ctx context.Context, order Order) error {
    // 如果 InventoryCheck 卡住 30 秒,goroutine 就白白等 30 秒
    ok, err := InventoryCheck(ctx, order.Items)
    if err != nil {
        return err
    }
    // 如果支付服务超时,前面已经浪费了 30 秒,用户早已放弃
    err = Charge(ctx, order.Total)
    if err != nil {
        return err
    }
    // 通知服务也卡住……goroutine 泄漏达到上限 → OOM
    return Notify(ctx, order.UserID)
}

(2) Go 的解法:Context

GO
// context_demo.go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // 根 Context
    root := context.Background()

    // 用 WithTimeout 包裹:2 秒超时
    ctx, cancel := context.WithTimeout(root, 2*time.Second)
    defer cancel()  // 确保资源释放

    result := PlaceOrder(ctx, "order-123")
    fmt.Println(result)
}

func PlaceOrder(ctx context.Context, orderID string) string {
    // 检查 Context 是否已取消
    select {
    case <-ctx.Done():
        return fmt.Sprintf("取消: %v", ctx.Err())
    default:
    }

    // 给每个下游调用分配更短的超时
    checkCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    ok := InventoryCheck(checkCtx, orderID)
    if !ok {
        return "库存不足"
    }
    return "下单成功"
}

func InventoryCheck(ctx context.Context, orderID string) bool {
    // 模拟耗时调用
    select {
    case <-time.After(500 * time.Millisecond):
        return true
    case <-ctx.Done():
        fmt.Printf("InventoryCheck 被取消: %v\n", ctx.Err())
        return false
    }
}

输出(正常):

TEXT 📖 仅展示
下单成功

(3) 收益:有 Context vs 无 Context

情况 结果
无超时控制 goroutine 泄漏,系统 OOM
手动 time.After 检查 代码分散,每个函数自己写超时
Context 统一控制 父 Context 取消,子全部级联取消

3. 根 Context

GO
package main

import (
    "context"
    "fmt"
)

func main() {
    // Background():根 Context,永远不会取消
    // 用于 main 函数、初始化、顶层请求
    ctx := context.Background()
    fmt.Printf("Background: %v\n", ctx)

    // TODO():当不确定用什么 Context 时,先用 TODO 占位
    // 标记尚未接入 Context 的代码需要重构
    todo := context.TODO()
    fmt.Printf("TODO: %v\n", todo)
}

(1) Background vs TODO

Context 用途 是否会取消
Background() 根节点,所有 Context 的起点 永不
TODO() 占位符,表示代码还未接入 Context 永不
💡 提示: context.Background() 是所有 Context 树的根节点,永远不被取消。context.TODO() 用于标记尚未接入 Context 链的代码——你应该尽快将其替换为合适的 Context。


4. context.WithCancel 手动取消

▶ 示例:手动取消 goroutine

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func Worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d 停止: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d 工作中...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go Worker(ctx, 1)
    go Worker(ctx, 2)

    time.Sleep(2 * time.Second)
    fmt.Println("主 goroutine 发起取消...")
    cancel()  // 告诉所有 Worker 停止

    // 等待 goroutine 退出
    time.Sleep(500 * time.Millisecond)
}
▶ 试一试

▶ 示例:级联取消

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func handler(ctx context.Context) {
    // 子 Context 继承父 Context
    childCtx, cancel := context.WithCancel(ctx)
    defer cancel()

    go subTask(childCtx, "task-1")
    go subTask(childCtx, "task-2")

    // 父 Context 取消 → 子 Context 自动取消
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("Handler 完成")
    case <-ctx.Done():
        fmt.Println("Handler 被取消")
    }
}

func subTask(ctx context.Context, name string) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Printf("%s 完成\n", name)
    case <-ctx.Done():
        fmt.Printf("%s 被取消: %v\n", name, ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go handler(ctx)

    time.Sleep(500 * time.Millisecond)
    cancel()  // 取消 → handler → subTask 全部级联取消

    time.Sleep(1 * time.Second)
}
▶ 试一试
100%
sequenceDiagram
    participant Main as main()
    participant H as handler
    participant ST1 as subTask-1
    participant ST2 as subTask-2

    Main->>H: WithCancel
    H->>ST1: WithCancel
    H->>ST2: WithCancel
    Note over Main,ST2: 正常执行
    Main->>Main: cancel()
    Main-->>H: ctx.Done()
    H-->>ST1: ctx.Done()
    H-->>ST2: ctx.Done()
    Note over Main,ST2: 全部级联取消
🔥 易错: cancel() 必须被调用。即使你用了 WithTimeout,也必须 defer cancel()。否则 Context 的资源(定时器、goroutine)不会被释放。规则:创建 WithCancel / WithTimeout / WithDeadline → 立即 defer cancel()。


5. context.WithTimeout 超时自动取消

▶ 示例:超时控制

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func callExternalAPI(ctx context.Context, name string, delay time.Duration) (string, error) {
    select {
    case <-time.After(delay):
        return fmt.Sprintf("%s 响应", name), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // 1 秒超时
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // 调用两个下游服务
    result1 := make(chan string, 1)
    result2 := make(chan string, 1)

    go func() {
        r, err := callExternalAPI(ctx, "服务A", 500*time.Millisecond)
        if err != nil {
            result1 <- fmt.Sprintf("服务A 失败: %v", err)
            return
        }
        result1 <- r
    }()

    go func() {
        r, err := callExternalAPI(ctx, "服务B", 1500*time.Millisecond)
        if err != nil {
            result2 <- fmt.Sprintf("服务B 失败: %v", err)
            return
        }
        result2 <- r
    }()

    fmt.Println(<-result1)  // 服务A 响应(500ms < 1s 超时)
    fmt.Println(<-result2)  // 服务B 失败: context deadline exceeded(1500ms > 1s)
}
▶ 试一试

▶ 示例:WithTimeout vs WithDeadline

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // WithTimeout:从现在开始 2 秒后超时
    timeoutCtx, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel1()

    // WithDeadline:指定绝对时间
    deadline := time.Now().Add(2 * time.Second)
    deadlineCtx, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()

    // 两者效果相同
    fmt.Printf("timeoutCtx deadline: %v\n", timeoutCtx.Deadline())
    fmt.Printf("deadlineCtx deadline: %v\n", deadlineCtx.Deadline())
}
▶ 试一试

(2) WithTimeout vs WithDeadline

方法 参数 用途
WithTimeout(parent, 2*time.Second) 相对时间 最常用,"最多等 2 秒"
WithDeadline(parent, time.Time) 绝对时间 指定截止点,"在 15:30 前完成"

6. context.WithValue 请求级别传值

▶ 示例:WithValue

GO
package main

import (
    "context"
    "fmt"
)

// 自定义 key 类型(避免冲突)
type contextKey string

const (
    UserIDKey    contextKey = "user_id"
    TraceIDKey   contextKey = "trace_id"
    RequestIDKey contextKey = "request_id"
)

func middleware(ctx context.Context) context.Context {
    // 从请求头获取 trace ID
    ctx = context.WithValue(ctx, TraceIDKey, "trace-123")
    ctx = context.WithValue(ctx, RequestIDKey, "req-456")
    return ctx
}

func handler(ctx context.Context, userID string) {
    ctx = context.WithValue(ctx, UserIDKey, userID)

    // 传递给业务层
    service(ctx)
}

func service(ctx context.Context) {
    // 从 Context 取值
    userID := ctx.Value(UserIDKey).(string)
    traceID := ctx.Value(TraceIDKey).(string)
    requestID := ctx.Value(RequestIDKey).(string)

    fmt.Printf("处理请求: user=%s, trace=%s, request=%s\n",
        userID, traceID, requestID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx)
    handler(ctx, "user-007")
}
▶ 试一试
🔥 易错: context.WithValue 的 key 必须使用自定义类型,不能直接用 string。如果两个包都用 "user_id" 字符串作 key,就会冲突。自定义类型 type contextKey string 是最佳实践。

(2) WithValue 适用场景

场景 推荐 不推荐
TraceID / RequestID ✅ WithValue 传递 ❌ 全局变量
认证 Token ✅ WithValue 传递 ❌ 函数参数
数据库连接 ❌ 从依赖注入获取 ❌ WithValue
业务参数 ❌ 显式参数 ❌ WithValue 隐式传递

7. Context 链式传递规则

GO
package main

import (
    "context"
    "fmt"
    "time"
)

type contextKey string

func main() {
    root := context.Background()

    // 链式:WithCancel → WithTimeout → WithValue
    ctx1, cancel1 := context.WithCancel(root)
    defer cancel1()

    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
    defer cancel2()

    ctx3 := context.WithValue(ctx2, contextKey("trace"), "trace-007")

    // ctx3 继承了 ctx1 的取消 + ctx2 的超时 + ctx3 的值
    fmt.Printf("ctx3 deadline: %v\n", ctx3.Deadline())
    fmt.Printf("ctx3 value: %v\n", ctx3.Value(contextKey("trace")))

    // 先取消 ctx1 → ctx2 和 ctx3 都会收到取消信号
    cancel1()
    time.Sleep(10 * time.Millisecond)
    fmt.Printf("ctx2 err: %v\n", ctx2.Err())
    fmt.Printf("ctx3 err: %v\n", ctx3.Err())
}

(1) Context 传递规则

规则 说明
第一个参数 函数签名的第一个参数永远是 ctx context.Context
不存 struct Context 不要存在 struct 字段里,而是作为参数传递
函数间传递 每个需要感知取消/超时的函数都接收 Context
不可变链 每次 WithCancel/WithTimeout/WithValue 都返回新 Context
级联取消 父取消 → 子全部取消;子取消不影响父

8. 完整示例:微服务链路超时控制

GO
// microservice_chain.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// ---------- 模拟下游服务 ----------

// 库存服务
type InventoryService struct{}

func (s *InventoryService) Check(ctx context.Context, orderID string) (bool, error) {
    // 模拟随机延迟
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return true, nil
    case <-ctx.Done():
        return false, ctx.Err()
    }
}

// 支付服务
type PaymentService struct{}

func (s *PaymentService) Charge(ctx context.Context, amount float64) (string, error) {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return "pay-" + fmt.Sprintf("%d", time.Now().UnixNano()), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

// 通知服务
type NotificationService struct{}

func (s *NotificationService) Send(ctx context.Context, userID, message string) error {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// ---------- 业务层 ----------

type OrderService struct {
    inventory *InventoryService
    payment   *PaymentService
    notify    *NotificationService
}

func NewOrderService() *OrderService {
    return &OrderService{
        inventory: &InventoryService{},
        payment:   &PaymentService{},
        notify:    &NotificationService{},
    }
}

// PlaceOrder 使用 Context 控制整个链路超时
func (s *OrderService) PlaceOrder(ctx context.Context, userID, orderID string, amount float64) error {
    // 1. 库存检查(最多等 1 秒)
    invCtx, invCancel := context.WithTimeout(ctx, 1*time.Second)
    defer invCancel()

    ok, err := s.inventory.Check(invCtx, orderID)
    if err != nil {
        return fmt.Errorf("库存检查失败: %w", err)
    }
    if !ok {
        return fmt.Errorf("库存不足")
    }

    // 2. 支付(最多等 2 秒)
    payCtx, payCancel := context.WithTimeout(ctx, 2*time.Second)
    defer payCancel()

    paymentID, err := s.payment.Charge(payCtx, amount)
    if err != nil {
        return fmt.Errorf("支付失败: %w", err)
    }

    // 3. 通知(最多等 500ms)
    notifyCtx, notifyCancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer notifyCancel()

    err = s.notify.Send(notifyCtx, userID, "下单成功: "+orderID)
    if err != nil {
        // 通知失败不影响订单(异步日志记录)
        fmt.Printf("通知失败(已记录): %v\n", err)
    }

    fmt.Printf("下单成功: user=%s, order=%s, payment=%s\n", userID, orderID, paymentID)
    return nil
}

// ---------- 客户端 ----------

func main() {
    svc := NewOrderService()

    // 整体请求超时 3 秒
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := svc.PlaceOrder(ctx, "user-007", "order-123", 99.99)
    if err != nil {
        fmt.Printf("下单失败: %v\n", err)
    }
}
💡 提示: Context 的 Err() 返回两种可能:context.Canceled(手动取消)或 context.DeadlineExceeded(超时)。在链路中你可以通过 errors.Is(err, context.DeadlineExceeded) 判断是哪种取消,从而决定重试策略。


❓ 常见问题

Q Context 什么时候用?
A 所有需要超时控制、手动取消、请求级别传值的场景。包括:HTTP 请求处理、数据库查询、RPC 调用、定时任务。不要把 Context 用在不需要取消或超时的纯计算函数中。
Q Background() 和 TODO() 有什么区别?
A 两者都是永不取消的根 Context,语义不同:Background() 是你代码中正式使用的根;TODO() 是占位符,表示这段代码还没接入 Context,需要你尽快重构。
Q WithTimeout 和 WithDeadline 选哪个?
A 大多数情况用 WithTimeout("最多等 2 秒"更直观)。只有当你的超时点是绝对时间时("在 15:30:00 前完成")才用 WithDeadline。WithTimeout 底层调用的就是 WithDeadline。
Q Context 能取消多次吗?
A 取消只能触发一次——重复调用 cancel() 是安全的(第二次及后续调用是 no-op)。所有监听 Done() 的 goroutine 只会收到一次信号。
Q parent cancel 和 timeout 同时发生时怎么办?
A 两者独立触发,先到者生效。如果父 Context 手动取消,所有子 Context 立刻收到 Done() 信号,即使子 Context 的超时还没到。这确保了级联取消的最短路径。
Q WithValue 传递的值能修改吗?
A 不能。context.WithValue 的值是不可变的(immutable)。所谓"修改"本质是创建一个新 Context。子 Context 的 WithValue 不会影响父 Context。这保证了并发安全。
Q Context 适合存储什么类型的数据?
A 只适合存储请求级别的元数据:TraceID、RequestID、UserID、认证 Token。不适合存储业务参数(如 price、quantity),也不适合存储数据库连接或配置——那些应该通过依赖注入传递。

📖 小节


📝 作业

  1. 基础题(难度⭐):写一个函数 FetchWithTimeout(ctx, url string, timeout time.Duration),使用 context.WithTimeout 实现 HTTP 请求超时控制。超时后应自动取消到底层 HTTP 请求。

  2. 进阶题(难度⭐⭐):实现一个可并发取消的定时任务管理器。支持:(1) 注册多个定时任务(每个 goroutine);(2) 单个取消(调用 cancel 函数);(3) 批量取消(WithCancel 级联);(4) 所有任务共享一个根 Context。

  3. 挑战题(难度⭐⭐⭐):模拟分布式链路追踪系统。要求:(1) 用 WithValue 传递 TraceID 跨 3 层函数调用(API → Service → DB);(2) 每层有自己的超时控制(Service 2s, DB 500ms);(3) 超时后当前层取消但不影响上游;(4) 输出每个阶段的耗时和 TraceID。用 Context 的 Deadline() 计算剩余时间。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏