Go: Go Context 上下文
最后更新:2026-08-26
Context 是 Go 并发控制的基石——它让超时、取消和传值在 goroutine 调用链中优雅传递。
当你的服务需要同时处理几十个外部依赖时(每个依赖都有自己的超时和取消逻辑),如何用统一的方式管理它们?这节课你将掌握 Go context 包的完整用法。
1. 你将学到
context.Background()和context.TODO()context.WithCancel手动取消context.WithTimeout超时自动取消context.WithDeadline截止时间取消context.WithValue请求级别传值- context 链式传递规则
- 实战:微服务链路超时级联取消
2. 一个后端工程师的真实故事
(1) 痛点:一个上游超时,整个系统雪崩
Xiaoli 是订单系统的工程师,她负责的 REST API dependency 3 个下游服务:
"下单接口调用了 3 个服务:库存服务、支付服务、通知服务。有一天库存服务卡了 20 秒才响应,结果我的服务内存暴涨,所有 goroutine 都在等它——其他用户的请求也进不来了。老板问'为什么下单页面全挂了?'"
问题分析:
// 坏代码:没有超时控制
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
// 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
}
}
输出(正常):
下单成功
(3) 收益:有 Context vs 无 Context
| 情况 | 结果 |
|---|---|
| 无超时控制 | goroutine 泄漏,系统 OOM |
| 手动 time.After 检查 | 代码分散,每个函数自己写超时 |
| Context 统一控制 | 父 Context 取消,子全部级联取消 |
3. 根 Context
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
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)
}
▶ 示例:级联取消
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)
}
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 超时自动取消
▶ 示例:超时控制
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
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
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")
}
"user_id" 字符串作 key,就会冲突。自定义类型 type contextKey string 是最佳实践。
(2) WithValue 适用场景
| 场景 | 推荐 | 不推荐 |
|---|---|---|
| TraceID / RequestID | ✅ WithValue 传递 | ❌ 全局变量 |
| 认证 Token | ✅ WithValue 传递 | ❌ 函数参数 |
| 数据库连接 | ❌ 从依赖注入获取 | ❌ WithValue |
| 业务参数 | ❌ 显式参数 | ❌ WithValue 隐式传递 |
7. Context 链式传递规则
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. 完整示例:微服务链路超时控制
// 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)
}
}
Err() 返回两种可能:context.Canceled(手动取消)或 context.DeadlineExceeded(超时)。在链路中你可以通过 errors.Is(err, context.DeadlineExceeded) 判断是哪种取消,从而决定重试策略。
❓ 常见问题
📖 小节
- Background() / TODO():两种根 Context,不可取消
- WithCancel:手动控制 goroutine 取消
- WithTimeout / WithDeadline:超时自动取消
- WithValue:请求级别值传递(自定义 key 类型)
- 链式传递:第一个参数永远是 Context
- 级联取消:父取消 → 子全部取消
cancel()必须 defer 调用- 实战:微服务链路超时级联取消
📝 作业
-
基础题(难度⭐):写一个函数
FetchWithTimeout(ctx, url string, timeout time.Duration),使用context.WithTimeout实现 HTTP 请求超时控制。超时后应自动取消到底层 HTTP 请求。 -
进阶题(难度⭐⭐):实现一个可并发取消的定时任务管理器。支持:(1) 注册多个定时任务(每个 goroutine);(2) 单个取消(调用 cancel 函数);(3) 批量取消(WithCancel 级联);(4) 所有任务共享一个根 Context。
-
挑战题(难度⭐⭐⭐):模拟分布式链路追踪系统。要求:(1) 用 WithValue 传递 TraceID 跨 3 层函数调用(API → Service → DB);(2) 每层有自己的超时控制(Service 2s, DB 500ms);(3) 超时后当前层取消但不影响上游;(4) 输出每个阶段的耗时和 TraceID。用 Context 的
Deadline()计算剩余时间。