强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

16 - Context:取消传播、超时控制、值传递

16 - Context

16.1 Context 概述

context.Context 是 Go 并发编程的核心原语,用于在 goroutine 之间传递取消信号、截止时间和请求范围的值。

type Context interface {
    Deadline() (deadline time.Time, ok bool)  // 截止时间
    Done() <-chan struct{}                     // 取消信号 channel
    Err() error                                // 取消原因
    Value(key any) any                         // 取值
}

16.2 创建 Context

package main

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

func main() {
    // 1. Background:根 context,永不取消
    ctx := context.Background()

    // 2. TODO:标记待处理的 context
    ctx2 := context.TODO()

    // 3. WithCancel:可手动取消
    ctx3, cancel := context.WithCancel(ctx)
    defer cancel()

    // 4. WithTimeout:超时自动取消
    ctx4, cancel4 := context.WithTimeout(ctx, 5*time.Second)
    defer cancel4()

    // 5. WithDeadline:指定时间取消
    deadline := time.Now().Add(10 * time.Second)
    ctx5, cancel5 := context.WithDeadline(ctx, deadline)
    defer cancel5()

    // 6. WithValue:附加键值对
    ctx6 := context.WithValue(ctx, "requestID", "abc-123")

    fmt.Println(ctx, ctx2, ctx3, ctx4, ctx5, ctx6)
}
创建方式说明使用场景
Background()根 context,不取消main、init、测试
TODO()占位符不确定时使用
WithCancel()手动取消需要主动取消
WithTimeout()超时取消HTTP 请求超时
WithDeadline()定时取消精确时间控制
WithValue()传递值请求 ID、认证信息

16.3 取消传播

package main

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

func worker(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("[%s] 收到取消信号: %v\n", name, ctx.Err())
            return
        default:
            fmt.Printf("[%s] 工作中...\n", name)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // 父 context 取消时,所有子 context 也会取消
    ctx, cancel := context.WithCancel(context.Background())

    go worker(ctx, "worker-1")
    go worker(ctx, "worker-2")

    // 子 context
    childCtx, childCancel := context.WithCancel(ctx)
    go worker(childCtx, "child-worker")

    time.Sleep(2 * time.Second)

    // 取消父 context(所有子 context 也会被取消)
    fmt.Println("取消父 context...")
    cancel()

    // 即使手动调用子 cancel 也不影响父 context
    childCancel()

    time.Sleep(1 * time.Second)
    fmt.Println("完成")
}

16.4 超时控制

package main

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

func slowOperation(ctx context.Context) (string, error) {
    // 模拟耗时操作
    select {
    case <-time.After(3 * time.Second):
        return "操作完成", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

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

    result, err := slowOperation(ctx)
    if err != nil {
        fmt.Println("错误:", err) // 错误: context deadline exceeded
    } else {
        fmt.Println("结果:", result)
    }
}

HTTP 请求超时

func fetchURL(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

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

    return io.ReadAll(resp.Body)
}

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

    data, err := fetchURL(ctx, "https://api.example.com/data")
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    fmt.Println("数据:", string(data))
}

16.5 值传递

package main

import (
    "context"
    "fmt"
)

// 定义专用类型避免键冲突
type contextKey string

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

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

func GetRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(requestIDKey).(string); ok {
        return id
    }
    return ""
}

func WithUserID(ctx context.Context, id int) context.Context {
    return context.WithValue(ctx, userIDKey, id)
}

func GetUserID(ctx context.Context) int {
    if id, ok := ctx.Value(userIDKey).(int); ok {
        return id
    }
    return 0
}

func handler(ctx context.Context) {
    reqID := GetRequestID(ctx)
    userID := GetUserID(ctx)
    fmt.Printf("处理请求: RequestID=%s, UserID=%d\n", reqID, userID)
}

func main() {
    ctx := context.Background()
    ctx = WithRequestID(ctx, "req-abc-123")
    ctx = WithUserID(ctx, 42)
    handler(ctx)
}

⚠️ 注意:Context 值应该只传递请求范围的数据(如请求 ID、认证令牌),不要传递业务参数。

16.6 实际应用模式

服务优雅关闭

func server() {
    ctx, cancel := signal.NotifyContext(context.Background(), 
        syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    srv := &http.Server{Addr: ":8080"}

    go func() {
        <-ctx.Done()
        shutdownCtx, shutdownCancel := context.WithTimeout(
            context.Background(), 10*time.Second)
        defer shutdownCancel()
        srv.Shutdown(shutdownCtx)
    }()

    srv.ListenAndServe()
}

并发请求

func fetchAll(ctx context.Context, urls []string) []Result {
    results := make([]Result, len(urls))
    var wg sync.WaitGroup

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            data, err := fetchURL(ctx, url)
            results[i] = Result{Data: data, Err: err}
        }(i, url)
    }

    wg.Wait()
    return results
}

🏢 业务场景

  1. HTTP 请求链路:携带请求 ID、超时控制
  2. 数据库查询:context 传递超时和取消信号
  3. 微服务调用:传播取消信号到下游服务
  4. 批量任务:父 context 取消时终止所有子任务
  5. 优雅关闭:监听系统信号,传播取消

📖 扩展阅读