context 包
context.Context 是 Go 语言中用于管理 Goroutine 生命周期、传递请求域数据和取消信号的接口。它是 Go 并发编程的”控制中枢”,在 HTTP 请求处理、数据库调用、RPC 通信等场景中不可或缺。通过 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 的传播规则
- 取消信号会向下传播:父 Context 被取消时,所有子 Context 自动取消
- 取消信号不会向上传播:子 Context 取消不影响父 Context
Value()会沿链向上查找:子 Context 查不到会自动到父 Context 查找- Context 是不可变的:每次创建新 Context 都会返回一个新的值
- 一旦 Context 被取消,不可恢复
context.Background() 与 context.TODO()
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,与 Background() 功能完全相同。但它有不同的语义——当不确定该使用什么 Context 时使用。代码审查工具和 linter 可以通过 TODO 标识需要后续改进的代码。
Background vs TODO 的选择
- 使用
context.Background():当你明确知道这是最顶层的 Context(main、初始化、测试) - 使用
context.TODO():当你不确定该用什么 Context,暂时占位(后续需要修改) - 两者在功能上完全相同,区别仅在于语义
使用约定
// ✅ 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(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 canceledWithCancel 的最佳实践
- 总是调用 cancel():即使没有手动取消,也要用
defer cancel()确保资源释放 - cancel 是幂等的:多次调用
cancel()不会 panic,但只有第一次有效 - 尽早创建,尽早 defer:在创建 context 后立即
defer cancel() - cancel 函数不需要传给子函数:子函数只需要接收
ctx参数
context.WithTimeout() 与 context.WithDeadline()
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(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
| 特性 | WithTimeout | WithDeadline |
|---|---|---|
| 参数 | 相对时长(time.Duration) | 绝对时间(time.Time) |
| 场景 | ”最多等待 N 秒" | "必须在时刻 T 之前完成” |
| 内部实现 | 基于 WithDeadline | 底层实现 |
| 使用频率 | 更常用 | 较少直接使用 |
超时后的资源清理
Context 超时或取消后,你的代码应该立即停止工作并返回,释放所有资源(关闭文件、数据库连接等)。Context 本身不会帮你做这些清理工作,它只是提供一个信号:
func fetchData(ctx context.Context) ([]byte, error) {
resp, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// 当 ctx 取消时,http.RequestWithContext 会自动取消 HTTP 请求
body, err := http.DefaultClient.Do(resp)
if err != nil {
return nil, err // 可能是 context.DeadlineExceeded
}
defer body.Close()
return io.ReadAll(body)
}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-defWithValue 的注意事项
- 使用自定义类型作为 key:不要使用内置类型(
string、int)作为 key,不同包可能使用相同的字符串导致冲突 - 不要存储可选参数:Context 应用于传递请求域数据(如 request ID),而不是函数的可选参数
- 值应该是不可变的:Context 中的值应该是只读的,不应该被修改
- 避免存储敏感信息:Context 值在整个调用链中可见,不要存储密码等敏感信息
- 性能考虑:
Value()沿链查找,嵌套过深会影响性能
自定义 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
}
}Context 使用总结口诀
- 传而不存:Context 作为参数传递,不存储在结构体中
- 首参为 Context:Context 应该是函数的第一个参数,通常命名为
ctx - 不传 nil:如果不确定用什么 Context,用
context.TODO()而非nil - 值用自定义类型:WithValue 的 key 使用自定义类型避免冲突
- 始终 defer cancel:确保 cancel 函数被调用,释放资源
练习题
练习 1:超时控制的数据库查询
实现一个 QueryWithTimeout 函数,接收 context.Context 和查询参数。要求:
- 使用
context.WithTimeout设置 3 秒超时 - 模拟数据库查询(使用
time.Sleep) - 超时后返回
context.DeadlineExceeded错误 - 查询成功后返回结果
代码:
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 中添加不同的值:
RequestIDMiddleware:添加x-request-idAuthMiddleware:添加user_id和roleLoggingMiddleware:记录请求处理时间
最终处理器从 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 的取消传播规则:
- 创建一个父 Context(WithCancel)
- 从父 Context 创建 3 个子 Context
- 从其中一个子 Context 创建 2 个孙级 Context
- 调用其中一个子 Context 的 cancel
- 验证:被取消的子及其孙被取消,其他子不受影响
代码:
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 也不受影响(取消不向上传播 ✅)
