导航菜单

context 包

context.Context

context.Context 是 Go 语言中用于管理 Goroutine 生命周期、传递请求域数据和取消信号的接口。它是 Go 并发编程的”控制中枢”,在 HTTP 请求处理、数据库调用、RPC 通信等场景中不可或缺。通过 Context,你可以在调用链路中传播截止时间、取消信号和请求级别的元数据。

context.Context 的设计理念

核心思想

Context 设计哲学

Context 的核心设计理念是:“Context should flow through your program, not be stored in structs”。Context 应该作为函数的第一个参数在调用链中传递,而不是存储在结构体中。它本质上是一个只读的、树形的、不可变的值传递链

接口定义

// context 包中的核心接口
type Context interface {
    // 返回 context 被取消的时间(截止时间),如果没有设置则 ok 为 false
    Deadline() (deadline time.Time, ok bool)

    // 返回一个 channel,当 context 被取消或超时时该 channel 会被关闭
    Done() <-chan struct{}

    // 返回 context 关闭的原因
    Err() error

    // 获取 context 中存储的键值对
    Value(key any) any
}

Context 树形结构

context.Background()          context.TODO()
        │                           │
   WithCancel()              WithTimeout()
        │                           │
   childCtx1                childCtx2
   (可被取消)               (有超时)
        │                           │
   WithValue()              WithCancel()
        │                           │
   grandchild1             grandchild2
   (携带值)                 (可被取消)

context.Background() 与 context.TODO()

context.Background()

context.Background()

context.Background() 返回一个非 nil 的空 Context。它是所有 Context 树的根节点,永远不会被取消,没有截止时间,没有携带值。通常在 main 函数、初始化代码和测试中作为最顶层的 Context 使用。

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

    // Background 永远不会被取消
    fmt.Println(ctx.Deadline()) // 0001-01-01 ... false
    fmt.Println(ctx.Done())     // <nil> (channel 为 nil)
    fmt.Println(ctx.Err())      // nil
    fmt.Println(ctx.Value("key")) // nil
}

context.TODO()

context.TODO()

context.TODO() 也返回一个空 Context,与 Background() 功能完全相同。但它有不同的语义——当不确定该使用什么 Context 时使用。代码审查工具和 linter 可以通过 TODO 标识需要后续改进的代码。

使用约定

// ✅ main 函数中使用 Background
func main() {
    ctx := context.Background()
    srv := NewServer(ctx)
    srv.Run()
}

// ✅ 测试中使用 Background
func TestSomething(t *testing.T) {
    ctx := context.Background()
    result, err := DoSomething(ctx)
    // ...
}

// ✅ 函数签名中接受 Context 参数
func ProcessRequest(ctx context.Context, req *Request) (*Response, error) {
    // 使用传入的 context,而不是创建新的
}

// ❌ 不要在库函数中创建 Background
func BadFunction() {
    ctx := context.Background() // 不应该在这里创建
    // 应该由调用者传入 context
}

context.WithCancel()

context.WithCancel()

context.WithCancel(parent) 基于父 Context 创建一个可取消的子 Context。它返回子 Context 和一个 cancel 函数。调用 cancel() 会关闭子 Context 的 Done() channel,并向所有派生自该子 Context 的后代传播取消信号。

基本用法

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()
    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() {
    // 创建可取消的 context
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保资源释放

    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }

    // 3 秒后取消所有 worker
    time.Sleep(3 * time.Second)
    fmt.Println("发送取消信号...")
    cancel()

    wg.Wait()
    fmt.Println("所有 Worker 已退出")
}

取消传播

package main

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

func grandchild(ctx context.Context, name string) {
    <-ctx.Done()
    fmt.Printf("%s 收到取消: %v\n", name, ctx.Err())
}

func child(ctx context.Context, name string) {
    // 创建孙级 context
    gCtx, gCancel := context.WithCancel(ctx)
    defer gCancel()

    go grandchild(gCtx, name+".child")

    <-ctx.Done()
    fmt.Printf("%s 收到取消: %v\n", name, ctx.Err())
}

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

    // 创建两个子 context
    ctx1, _ := context.WithCancel(ctx)
    ctx2, _ := context.WithCancel(ctx)

    go child(ctx1, "Child-1")
    go child(ctx2, "Child-2")

    // 取消根 context
    cancel()

    time.Sleep(100 * time.Millisecond)
}
// 输出:
// Child-2 收到取消: context canceled
// Child-2.child 收到取消: context canceled
// Child-1 收到取消: context canceled
// Child-1.child 收到取消: context canceled

context.WithTimeout() 与 context.WithDeadline()

context.WithTimeout()

context.WithTimeout()

context.WithTimeout(parent, timeout) 创建一个在指定时间后自动取消的 Context。它在内部使用 WithDeadline 实现,截止时间为 time.Now().Add(timeout)。适用于”操作必须在 N 秒内完成”的场景。

package main

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

func slowOperation(ctx context.Context) error {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("操作完成")
        return nil
    case <-ctx.Done():
        return ctx.Err() // context.DeadlineExceeded
    }
}

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

    err := slowOperation(ctx)
    if err != nil {
        fmt.Println("操作失败:", err)
        // 输出: 操作失败: context deadline exceeded
    }
}

context.WithDeadline()

context.WithDeadline()

context.WithDeadline(parent, deadline) 创建一个在指定绝对时间自动取消的 Context。与 WithTimeout 不同,它接受一个绝对时间点而非时长。适用于”操作必须在某个时间点之前完成”的场景。

package main

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

func main() {
    // 设置绝对截止时间
    deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    // 检查剩余时间
    if d, ok := ctx.Deadline(); ok {
        remaining := time.Until(d)
        fmt.Printf("截止时间: %v, 剩余: %v\n", d, remaining.Round(time.Millisecond))
    }

    // 模拟工作
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("在截止时间内完成")
    case <-ctx.Done():
        fmt.Println("已超时:", ctx.Err())
    }
}

WithTimeout vs WithDeadline

特性WithTimeoutWithDeadline
参数相对时长(time.Duration绝对时间(time.Time
场景”最多等待 N 秒""必须在时刻 T 之前完成”
内部实现基于 WithDeadline底层实现
使用频率更常用较少直接使用

context.WithValue()

context.WithValue()

context.WithValue(parent, key, value) 创建一个携带键值对的子 Context。它不控制生命周期,只是向 Context 链中附加请求域的元数据(如请求 ID、用户信息、链路追踪 ID 等)。值通过 ctx.Value(key) 获取,会沿 Context 链向上查找。

基本用法

package main

import (
    "context"
    "fmt"
)

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

const (
    requestIDKey contextKey = "request_id"
    userIDKey    contextKey = "user_id"
    traceIDKey   contextKey = "trace_id"
)

func processRequest(ctx context.Context) {
    // 获取上下文值
    requestID := ctx.Value(requestIDKey)
    userID := ctx.Value(userIDKey)
    traceID := ctx.Value(traceIDKey)

    fmt.Printf("RequestID: %v\n", requestID)
    fmt.Printf("UserID: %v\n", userID)
    fmt.Printf("TraceID: %v\n", traceID)
}

func handleRequest(ctx context.Context) {
    // 向 context 中添加值
    ctx = context.WithValue(ctx, requestIDKey, "req-12345")
    ctx = context.WithValue(ctx, userIDKey, 42)
    ctx = context.WithValue(ctx, traceIDKey, "trace-abc-def")

    processRequest(ctx)
}

func main() {
    ctx := context.Background()
    handleRequest(ctx)
}
// 输出:
// RequestID: req-12345
// UserID: 42
// TraceID: trace-abc-def

自定义 Key 类型

// ✅ 正确:自定义类型作为 key
package mypkg

type key int

const (
    keyRequestID key = iota
    keyUserID
)

// ✅ 更好的做法:封装访问函数
func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, keyRequestID, id)
}

func RequestIDFrom(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(keyRequestID).(string)
    return id, ok
}

// 使用
ctx := mypkg.WithRequestID(ctx, "req-123")
if id, ok := mypkg.RequestIDFrom(ctx); ok {
    fmt.Println("Request ID:", id)
}
// ❌ 错误:使用字符串作为 key(容易冲突)
ctx := context.WithValue(ctx, "request_id", "123")
// 另一个包可能也使用 "request_id",覆盖你的值

在 HTTP 服务器中使用 Context

net/http 自动支持 Context

Go 的 net/http 包从 Go 1.7 开始原生支持 Context:

package main

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

func handler(w http.ResponseWriter, r *http.Request) {
    // 从请求中获取 context
    ctx := r.Context()

    // 模拟耗时操作
    select {
    case <-time.After(5 * time.Second):
        fmt.Fprintf(w, "操作完成")
    case <-ctx.Done():
        // 客户端断开连接时,ctx 会被取消
        fmt.Println("客户端断开:", ctx.Err())
        http.Error(w, "请求已取消", http.StatusRequestTimeout)
        return
    }
}

func main() {
    http.HandleFunc("/", handler)
    server := &http.Server{
        Addr:         ":8080",
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    // 使用 context 控制服务器优雅关闭
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Println("服务器错误:", err)
        }
    }()

    // 等待中断信号
    <-ctx.Done()

    // 优雅关闭:等待已有请求处理完毕
    shutdownCtx := context.WithValue(context.Background(), "reason", "shutdown")
    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("关闭错误:", err)
    }
    fmt.Println("服务器已关闭")
}

HTTP 客户端使用 Context

package main

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

func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    // 创建带 context 的请求
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }

    // 添加请求头
    req.Header.Set("X-Request-ID", "req-12345")

    // 发送请求(当 ctx 取消时,请求会自动中断)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("请求失败: %w", err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

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

    data, err := fetchWithTimeout(ctx, "https://httpbin.org/delay/1")
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    fmt.Printf("响应长度: %d bytes\n", len(data))
}

中间件中传递 Context

// 日志中间件:添加请求 ID
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 生成请求 ID
        requestID := generateRequestID()

        // 将请求 ID 添加到 context
        ctx := context.WithValue(r.Context(), "request_id", requestID)

        // 传递带有新 context 的请求
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 认证中间件:添加用户信息
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        user, err := authenticate(token)
        if err != nil {
            http.Error(w, "未授权", http.StatusUnauthorized)
            return
        }

        // 将用户信息添加到 context
        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 从 context 获取请求 ID 和用户信息
        requestID := r.Context().Value("request_id")
        user := r.Context().Value("user")
        fmt.Fprintf(w, "RequestID: %v, User: %v", requestID, user)
    })

    // 注册中间件
    handler := LoggingMiddleware(AuthMiddleware(mux))
    http.ListenAndServe(":8080", handler)
}

Context 的取消传播机制

传播方向

rootCtx (Background)

    ├── parentCtx (WithCancel)  ← cancel() 调用这里
    │       │
    │       ├── childCtx1 (WithValue)
    │       │       │
    │       │       └── grandchildCtx (WithTimeout)
    │       │
    │       └── childCtx2 (WithCancel)

    └── siblingCtx (WithTimeout)

当 parentCtx 被取消时:
  ✅ childCtx1 被取消
  ✅ grandchildCtx 被取消
  ✅ childCtx2 被取消
  ❌ siblingCtx 不受影响
  ❌ rootCtx 不受影响(Background 永不取消)

取消传播的完整示例

package main

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

func watch(ctx context.Context, name string, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("[%s] 已停止: %v\n", name, ctx.Err())
            return
        case <-time.After(300 * time.Millisecond):
            fmt.Printf("[%s] 运行中...\n", name)
        }
    }
}

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

    // 分支 A
    ctxA, cancelA := context.WithCancel(root)

    // 分支 B(有超时)
    ctxB, cancelB := context.WithTimeout(root, 4*time.Second)

    var wg sync.WaitGroup

    // 分支 A 的子节点
    ctxA1 := context.WithValue(ctxA, "name", "A-1")
    ctxA2 := context.WithValue(ctxA, "name", "A-2")

    wg.Add(4)
    go watch(ctxA1, "A-1", &wg)
    go watch(ctxA2, "A-2", &wg)
    go watch(ctxB, "B", &wg)

    // 2 秒后取消分支 A
    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("--- 取消分支 A ---")
        cancelA()
    }()

    // 4 秒后分支 B 自动超时
    wg.Wait()
    cancelB() // 虽然 B 已超时,但仍需调用 cancel 释放资源
}

Context 错误类型

// Context 取消后,ctx.Err() 返回以下错误之一:

// context.Canceled —— 由 WithCancel 的 cancel() 触发
// var Canceled = errors.New("context canceled")

// context.DeadlineExceeded —— 由 WithTimeout/WithDeadline 超时触发
// var DeadlineExceeded = errors.New("context deadline exceeded")

func handleContextError(ctx context.Context) error {
    select {
    case <-ctx.Done():
        if ctx.Err() == context.Canceled {
            fmt.Println("被主动取消")
        } else if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("超时")
        }
        return ctx.Err()
    default:
        return nil
    }
}

练习题

练习 1:超时控制的数据库查询

实现一个 QueryWithTimeout 函数,接收 context.Context 和查询参数。要求:

  1. 使用 context.WithTimeout 设置 3 秒超时
  2. 模拟数据库查询(使用 time.Sleep
  3. 超时后返回 context.DeadlineExceeded 错误
  4. 查询成功后返回结果
参考答案

代码

package main

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "time"
)

// 模拟数据库行
type Row struct {
    ID    int
    Name  string
    Email string
}

// QueryWithTimeout 带超时的数据库查询
func QueryWithTimeout(ctx context.Context, query string, args ...any) ([]Row, error) {
    // 创建 3 秒超时的 context
    queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    // 模拟数据库查询
    result := make(chan []Row, 1)
    errCh := make(chan error, 1)

    go func() {
        // 模拟耗时查询
        time.Sleep(2 * time.Second)
        rows := []Row{
            {ID: 1, Name: "Alice", Email: "alice@example.com"},
            {ID: 2, Name: "Bob", Email: "bob@example.com"},
        }
        result <- rows
    }()

    select {
    case rows := <-result:
        return rows, nil
    case <-queryCtx.Done():
        return nil, queryCtx.Err()
    }
}

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

    fmt.Println("=== 测试 1:查询在超时前完成 ===")
    rows, err := QueryWithTimeout(ctx, "SELECT * FROM users")
    if err != nil {
        fmt.Println("查询失败:", err)
    } else {
        for _, row := range rows {
            fmt.Printf("  ID: %d, Name: %s\n", row.ID, row.Name)
        }
    }

    fmt.Println("\n=== 测试 2:查询超时 ===")
    // 创建一个已超时的 context
    timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    // 等待 context 超时
    time.Sleep(2 * time.Second)

    rows, err = QueryWithTimeout(timeoutCtx, "SELECT * FROM users")
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("查询超时:", err)
        } else {
            fmt.Println("其他错误:", err)
        }
    } else {
        fmt.Println("结果:", rows)
    }
}

输出

=== 测试 1:查询在超时前完成 ===
  ID: 1, Name: Alice
  ID: 2, Name: Bob

=== 测试 2:查询超时 ===
查询超时: context deadline exceeded

练习 2:Context 值传递的中间件链

实现一个 HTTP 中间件链,包含以下三个中间件,每个中间件向 Context 中添加不同的值:

  1. RequestIDMiddleware:添加 x-request-id
  2. AuthMiddleware:添加 user_idrole
  3. LoggingMiddleware:记录请求处理时间

最终处理器从 Context 中读取这些值并返回。

参考答案

代码

package main

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

// ---- 自定义 Key 类型 ----
type key int

const (
    keyRequestID key = iota
    keyUserID
    keyUserRole
    keyStartTime
)

// ---- 辅助函数 ----
func GetRequestID(ctx context.Context) string {
    id, _ := ctx.Value(keyRequestID).(string)
    return id
}

func GetUserID(ctx context.Context) int {
    id, _ := ctx.Value(keyUserID).(int)
    return id
}

func GetUserRole(ctx context.Context) string {
    role, _ := ctx.Value(keyUserRole).(string)
    return role
}

// ---- 中间件 ----

func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = fmt.Sprintf("req-%d", time.Now().UnixNano())
        }
        ctx := context.WithValue(r.Context(), keyRequestID, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 模拟认证
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "缺少 Authorization 头", http.StatusUnauthorized)
            return
        }

        // 模拟解析 token 得到用户信息
        ctx := context.WithValue(r.Context(), keyUserID, 42)
        ctx = context.WithValue(ctx, keyUserRole, "admin")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        ctx := context.WithValue(r.Context(), keyStartTime, start)

        next.ServeHTTP(w, r.WithContext(ctx))

        // 记录处理时间
        duration := time.Since(start)
        requestID := GetRequestID(ctx)
        fmt.Printf("[%s] %s %s - %v\n", requestID, r.Method, r.URL.Path, duration)
    })
}

// ---- 处理器 ----
func profileHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    requestID := GetRequestID(ctx)
    userID := GetUserID(ctx)
    role := GetUserRole(ctx)

    response := fmt.Sprintf(
        "RequestID: %s\nUserID: %d\nRole: %s\n",
        requestID, userID, role,
    )
    w.Write([]byte(response))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/profile", profileHandler)

    // 中间件链:从外到内
    handler := LoggingMiddleware(AuthMiddleware(RequestIDMiddleware(mux)))

    fmt.Println("服务器启动在 :8080")
    http.ListenAndServe(":8080", handler)
}

测试

# 发送请求
$ curl -H "Authorization: Bearer token123" -H "X-Request-ID: my-request-001" http://localhost:8080/profile
RequestID: my-request-001
UserID: 42
Role: admin

# 服务器日志
[my-request-001] GET /profile - 1.2ms

练习 3:Context 取消传播实验

编写程序验证 Context 的取消传播规则:

  1. 创建一个父 Context(WithCancel)
  2. 从父 Context 创建 3 个子 Context
  3. 从其中一个子 Context 创建 2 个孙级 Context
  4. 调用其中一个子 Context 的 cancel
  5. 验证:被取消的子及其孙被取消,其他子不受影响
参考答案

代码

package main

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

type contextNode struct {
    name string
    ctx  context.Context
}

func monitor(ctx context.Context, name string, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    <-ctx.Done()
    results <- fmt.Sprintf("[%s] 已取消: %v", name, ctx.Err())
}

func main() {
    // 创建父 Context
    parentCtx, parentCancel := context.WithCancel(context.Background())
    defer parentCancel()

    // 创建 3 个子 Context
    childCtx1, childCancel1 := context.WithCancel(parentCtx)
    childCtx2, childCancel2 := context.WithCancel(parentCtx)
    childCtx3, childCancel3 := context.WithCancel(parentCtx)

    // 从 child1 创建 2 个孙级 Context
    grandchildCtx1, _ := context.WithCancel(childCtx1)
    grandchildCtx2, _ := context.WithCancel(childCtx1)

    results := make(chan string, 10)
    var wg sync.WaitGroup

    nodes := []struct {
        ctx  context.Context
        name string
    }{
        {parentCtx, "Parent"},
        {childCtx1, "Child-1"},
        {childCtx2, "Child-2"},
        {childCtx3, "Child-3"},
        {grandchildCtx1, "Grandchild-1-1"},
        {grandchildCtx2, "Grandchild-1-2"},
    }

    for _, node := range nodes {
        wg.Add(1)
        go monitor(node.ctx, node.name, results, &wg)
    }

    // 等待 monitor 启动
    time.Sleep(100 * time.Millisecond)

    // 取消 Child-1
    fmt.Println(">>> 取消 Child-1")
    childCancel1()

    // 收集被取消的 context
    go func() {
        wg.Wait()
        close(results)
    }()

    time.Sleep(100 * time.Millisecond)

    fmt.Println("\n被取消的节点:")
    for result := range results {
        fmt.Println(" ", result)
    }

    // 验证 Child-2 和 Child-3 未被取消
    fmt.Println("\n验证:")
    if childCtx2.Err() == nil {
        fmt.Println("  ✅ Child-2 未被取消")
    } else {
        fmt.Println("  ❌ Child-2 被取消:", childCtx2.Err())
    }
    if childCtx3.Err() == nil {
        fmt.Println("  ✅ Child-3 未被取消")
    } else {
        fmt.Println("  ❌ Child-3 被取消:", childCtx3.Err())
    }

    childCancel2()
    childCancel3()
}

输出

>>> 取消 Child-1

被取消的节点:
  [Parent] 已取消: context canceled
  [Child-1] 已取消: context canceled
  [Child-2] 已取消: context canceled
  [Child-3] 已取消: context canceled
  [Grandchild-1-1] 已取消: context canceled
  [Grandchild-1-2] 已取消: context canceled

验证:
  ✅ Child-2 未被取消
  ✅ Child-3 未被取消

分析

  • 取消 Child-1 时,Grandchild-1-1 和 Grandchild-1-2 随之取消(子传孙 ✅)
  • Child-2 和 Child-3 不受影响(兄弟不受影响 ✅)
  • Parent Context 也不受影响(取消不向上传播 ✅)

搜索