← Select | sync 包 →

Context - 请求生命周期管理

Context 是 Go 用于控制 Goroutine 生命周期、传递请求范围数据的标准机制。掌握 Context 是编写可取消、可超时、可追踪的并发程序的关键。

📌 核心概念

🌳

树形结构

父子 Context 级联取消

Parent → Child
⏱️

超时控制

自动取消防止阻塞

WithTimeout
📋

数据传递

请求范围键值对

WithValue
🛑

手动取消

主动通知退出

WithCancel

Context 接口定义

📖 context.Context 接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

// 方法说明:
// Deadline: 返回截止时间,ok=true 表示设置了 deadline
// Done: 返回 channel,Context 取消时关闭
// Err: 返回取消原因 (Canceled 或 DeadlineExceeded)
// Value: 获取键值对数据

💡 Context 使用原则

  • 作为第一个参数: func DoWork(ctx context.Context, ...)
  • 不要存储在结构体: 显式传递,保持调用链清晰
  • 及时调用 cancel: 使用 defer cancel() 防止资源泄漏
  • 传递请求范围数据: 如 trace_id、user_id 等
  • 避免滥用: 不用于传递可选参数或配置

Context 层级结构

context.Background()
├──
WithTimeout(5s)
├──
WithValue("trace_id", "xxx")
└──
WithValue("user_id", "123")
├──
WithCancel()
└──
WithDeadline(t)
HTTP Request
创建 Root Context
WithTimeout 设置超时
超时或完成
自动取消所有子 Context
释放资源

四种 Context 类型

Context 类型对比
类型 创建方法 取消方式 使用场景
Background context.Background() 永不取消 根 Context,所有 Context 的祖先
TODO context.TODO() 永不取消 不确定使用何种 Context 时的临时选择
WithCancel ctx, cancel := WithCancel(parent) 手动调用 cancel() 需要主动取消的场景
WithTimeout ctx, cancel := WithTimeout(parent, d) 超时自动取消 防止操作无限等待
WithDeadline ctx, cancel := WithDeadline(parent, t) 到时间自动取消 指定具体截止时间
WithValue ctx := WithValue(parent, key, value) 继承父 Context 传递请求范围数据

基础用法

1. 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 working...\n", id)
            time.Sleep(time.Millisecond * 200)
        }
    }
}

func main() {
    // 创建可取消的 Context
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保资源释放

    // 启动多个 Worker
    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    // 运行 1 秒后取消
    time.Sleep(time.Second)
    fmt.Println("Cancelling all workers...")
    cancel() // 取消所有 Worker

    time.Sleep(time.Millisecond * 100)
}

2. WithTimeout - 超时控制

📝 防止操作无限等待

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

// 带超时的 HTTP 请求
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    // 创建带超时的 Context
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    // 创建带 Context 的请求
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return "", err
    }

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

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    return string(body), nil
}

func main() {
    body, err := fetchWithTimeout("https://api.example.com/data", time.Second*5)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Response: %s\n", body)
}

3. WithValue - 传递数据

📝 传递请求范围数据

package main

import (
    "context"
    "fmt"
)

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

const (
    userIDKey   contextKey = "userID"
    requestIDKey contextKey = "requestID"
)

// 处理请求
func handle(ctx context.Context) {
    userID := ctx.Value(userIDKey).(string)
    requestID := ctx.Value(requestIDKey).(string)
    
    fmt.Printf("Processing request %s for user %s\n", requestID, userID)
    
    // 调用下游服务,传递 Context
    callService(ctx)
}

func callService(ctx context.Context) {
    // 可以从 Context 中获取上游传递的数据
    requestID := ctx.Value(requestIDKey).(string)
    fmt.Printf("Service called with requestID: %s\n", requestID)
}

func main() {
    // 创建带数据的 Context
    ctx := context.WithValue(context.Background(), userIDKey, "user-123")
    ctx = context.WithValue(ctx, requestIDKey, "req-456")
    
    handle(ctx)
}

⚠️ WithValue 注意事项

  • key 使用自定义类型: 避免与其他包冲突
  • 只传递请求相关数据: 不用于传递配置或可选参数
  • 数据量要小: 避免在 Context 中存储大量数据
  • 注意类型安全: 使用类型断言时确保类型正确

生产级模式

1. HTTP 中间件中的 Context

📝 请求追踪中间件

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

type contextKey string

const (
    requestIDKey contextKey = "requestID"
    startTimeKey contextKey = "startTime"
)

// 请求追踪中间件
func requestLogger(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 生成 Request ID
        requestID := generateRequestID()
        
        // 创建带超时和数据的 Context
        ctx, cancel := context.WithTimeout(r.Context(), time.Second*30)
        defer cancel()
        
        ctx = context.WithValue(ctx, requestIDKey, requestID)
        ctx = context.WithValue(ctx, startTimeKey, time.Now())
        
        // 更新请求的 Context
        r = r.WithContext(ctx)
        
        // 调用下一个处理函数
        next(w, r)
        
        // 记录日志
        startTime := ctx.Value(startTimeKey).(time.Time)
        duration := time.Since(startTime)
        fmt.Printf("[%s] %s %s - %v\n", requestID, r.Method, r.URL.Path, duration)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    requestID := r.Context().Value(requestIDKey).(string)
    fmt.Printf("Handling request %s\n", requestID)
    fmt.Fprintln(w, "OK")
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

func main() {
    http.HandleFunc("/api/data", requestLogger(handler))
    http.ListenAndServe(":8080", nil)
}

2. 并发任务控制

📝 使用 errgroup 管理并发任务

package main

import (
    "context"
    "errors"
    "fmt"
    "sync"
    "time"
)

// 简单实现 errgroup
type Group struct {
    wg   sync.WaitGroup
    err  error
    once sync.Once
}

func (g *Group) Go(f func() error) {
    g.wg.Add(1)
    go func() {
        defer g.wg.Done()
        if err := f(); err != nil {
            g.once.Do(func() {
                g.err = err
            })
        }
    }()
}

func (g *Group) Wait() error {
    g.wg.Wait()
    return g.err
}

// 使用示例
func processWithErrGroup() error {
    g := &Group{}
    
    // 启动多个任务
    for i := 0; i < 5; i++ {
        i := i
        g.Go(func() error {
            fmt.Printf("Task %d starting\n", i)
            time.Sleep(time.Millisecond * 100)
            
            if i == 3 {
                return errors.New("task 3 failed")
            }
            fmt.Printf("Task %d completed\n", i)
            return nil
        })
    }
    
    return g.Wait()
}

func main() {
    if err := processWithErrGroup(); err != nil {
        fmt.Printf("Error: %v\n", err)
    }
}

3. 优雅关闭

📝 服务优雅关闭

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080"}
    
    // 启动 HTTP 服务
    go func() {
        fmt.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("Server error: %v\n", err)
        }
    }()
    
    // 监听退出信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    fmt.Println("Shutting down server...")
    
    // 创建带超时的 Context
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    
    // 优雅关闭
    if err := server.Shutdown(ctx); err != nil {
        fmt.Printf("Shutdown error: %v\n", err)
    }
    
    fmt.Println("Server stopped")
}

底层实现原理

Context 类型家族

📖 四种核心实现

// 1. emptyCtx - 空 Context (Background/TODO)
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return }
func (*emptyCtx) Done() <-chan struct{}                 { return nil }
func (*emptyCtx) Err() error                             { return nil }
func (*emptyCtx) Value(key interface{}) interface{}      { return nil }

// 2. cancelCtx - 可取消 Context (WithCancel)
type cancelCtx struct {
    Context
    mu       sync.Mutex
    done     atomic.Value
    children map[canceler]struct{}
    err      error
}

// 3. timerCtx - 超时 Context (WithTimeout/WithDeadline)
type timerCtx struct {
    cancelCtx
    timer *time.Timer // 超时自动取消
    deadline time.Time
}

// 4. valueCtx - 带值 Context (WithValue)
type valueCtx struct {
    Context
    key, val interface{}
}

cancelCtx 详细实现

📖 WithCancel 的实现

// context/context.go - cancelCtx 结构
type cancelCtx struct {
    Context
    mu       sync.Mutex
    done     atomic.Value  // 懒初始化,channel 在第一次使用时创建
    children map[canceler]struct{} // 子 Context 集合
    err      error         // 取消原因
}

// Done 返回 channel (懒加载)
func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    defer c.mu.Unlock()
    d := c.done.Load().(chan struct{})
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)
    }
    return d
}

// 取消操作
func (c *cancelCtx) cancel(err error, deliver bool) {
    c.mu.Lock()
    defer c.mu.Unlock()

    if c.err != nil {
        return // 已经取消
    }

    c.err = err
    if c.done.Load() == nil {
        c.done.Store(closedchan) // 使用预关闭的 channel
    } else {
        close(c.done.Load().(chan struct{}))
    }

    // 递归取消所有子 Context
    for child := range c.children {
        child.cancel(err, true)
    }
    c.children = nil
}

timerCtx 超时实现

📖 WithTimeout 的实现

type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    // 如果已经超时,直接返回
    if cur := time.Now(); deadline.After(cur) {
        c := &timerCtx{
            cancelCtx: *newCancelCtx(parent),
            deadline:  deadline,
        }
        
        // 设置定时器
        c.timer = time.AfterFunc(deadline.Sub(cur), func() {
            c.cancel(DeadlineExceeded, true)
        })
        
        return c, func() { c.cancel(Canceled, true) }
    }
    
    // 已经超时,返回已取消的 context
    return &cancelCtx{err: DeadlineExceeded, done: closedchan}, func{}{}
}

// 取消时停止定时器
func (c *timerCtx) cancel(err error, deliver bool) {
    c.cancelCtx.cancel(err, deliver)
    if c.timer != nil {
        c.timer.Stop()
    }
}

valueCtx 值传递实现

📖 WithValue 的实现

type valueCtx struct {
    Context
    key, val interface{}
}

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    return &valueCtx{parent, key, val}
}

// Value 方法沿树向上查找
func (c *valueCtx) Value(key interface{}) interface{} {
    if key == c.key {
        return c.val
    }
    return c.Context.Value(key) // 向父节点查找
}

取消传播流程图

调用 cancel()
设置 err
关闭 done channel
遍历 children
递归取消子 Context
所有子节点 Done() 返回
监听 Done() 的 Goroutine 收到信号
清理资源并退出

💡 Context 核心原理

  • 树形传播: 父 Context 取消时,递归取消所有子节点
  • 懒初始化: done channel 在第一次使用时才创建
  • 原子操作: 使用 atomic.Value 保证并发安全
  • 共享关闭 channel: 已取消的 Context 共享同一个 closedchan
  • 无锁读取: Done() 返回的 channel 只关闭不发送,读取无锁
  • Value 链式查找: 沿父节点链向上查找,O(n) 复杂度

常见陷阱与解决方案

Context 常见陷阱
陷阱 错误示例 正确做法
忘记调用 cancel 创建后不调用 cancel 使用 defer cancel()
存储 Context 在结构体 type S { ctx context.Context } 作为参数显式传递
传递 nil Context func f(ctx nil) 使用 context.TODO()
滥用 WithValue 传递大量数据或配置 只传递请求范围数据
忽略 Done channel 不检查 ctx.Done() 在循环中使用 select

🚨 资源泄漏示例

// ❌ 错误:忘记调用 cancel,导致资源泄漏
func leak() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    doWork(ctx) // 如果没有及时完成,goroutine 会泄漏
}

// ✅ 正确:使用 defer 确保 cancel 被调用
func noLeak() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    doWork(ctx)
}

最佳实践总结

✅ Context 使用原则

  • 作为第一个参数: func DoWork(ctx context.Context, ...)
  • 显式传递: 不要存储在结构体中
  • 及时取消: 使用 defer cancel()
  • 检查 Done: 在长时间运行中使用 select 检查
  • 传递必要数据: 只传递请求范围的数据
  • 设置超时: 外部调用应设置合理超时