Day 15|context:并发取消、超时与生命周期管理 [Go 语言 30 天系统学习计划]

学习目标

  • 理解 context 在 Go 并发中的设计初衷
  • 掌握 context 的取消、超时与截止时间机制
  • 理解 context 在 goroutine 树中的传播方式
  • 学会用 context 管理 goroutine 的生命周期

一、为什么需要 context

在前面的并发章节中,你已经看到几个现实问题:

  • goroutine 可能永久阻塞,无法退出
  • 请求超时后,后台 goroutine 仍在运行
  • 调用链很深时,如何统一取消所有子任务

这些问题的统一解决方案,就是 context。

context 的核心目标不是“传值”,而是:

  • 取消信号的传递
  • 超时 / 截止时间控制
  • 请求级生命周期管理

工程认知:context 是并发世界里的“生命线”。


二、context 的基本使用方式

context 定义在标准库:

import "context"

最常见的起点:

ctx := context.Background()

或:

ctx := context.TODO()

说明:

  • Background:顶层 context,通常用于 main / server 启动
  • TODO:占位用,表示“将来会补上正确的 context”

三、context 的取消机制(WithCancel)

创建一个可取消的 context:

ctx, cancel := context.WithCancel(context.Background())

取消 context:

cancel()

监听取消信号:

select {
case <-ctx.Done():
    fmt.Println("context cancelled")
    return
default:
    // 继续工作
}

关键点:

  • ctx.Done() 返回一个 channel
  • context 被取消时,该 channel 会被关闭

四、context 的超时与截止时间

WithTimeout:相对时间

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Second,
)
defer cancel()

WithDeadline:绝对时间

ctx, cancel := context.WithDeadline(
    context.Background(),
    time.Now().Add(time.Second),
)
defer cancel()

两者效果类似:

  • 时间到达后自动触发取消
  • ctx.Done() 会被关闭

工程习惯:所有带超时的 context 都应该 defer cancel()


五、context 的层级传播(父子关系)

context 是一棵树结构:

  • 子 context 继承父 context
  • 父 context 被取消,所有子 context 都会被取消
  • 子 context 取消,不影响父 context

示例:

parent := context.Background()
ctx1, cancel1 := context.WithCancel(parent)
ctx2, cancel2 := context.WithCancel(ctx1)

cancel1() // ctx1 和 ctx2 都会被取消

工程意义:一次取消,整条 goroutine 链同时退出。


六、context 在 goroutine 中的典型用法

context 通常作为函数的第一个参数:

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker exit")
            return
        default:
            // do work
        }
    }
}

启动 goroutine:

ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)

// 某个条件触发
cancel()

工程规则:

  • 不要在 goroutine 内创建“新的根 context”
  • context 应该从上层一路传下来

七、context.WithValue 的正确定位

context 可以携带少量请求级数据:

ctx = context.WithValue(ctx, "requestID", "abc123")

取值:

id := ctx.Value("requestID")

重要警告:

  • WithValue 不是用来传业务参数的
  • 只能存放请求范围内的元数据(traceID、userID)
  • key 必须使用自定义类型,避免冲突

推荐写法:

type ctxKeyRequestID struct{}

ctx = context.WithValue(ctx, ctxKeyRequestID{}, "abc123")

八、context 的常见误区

  • 把 context 存进 struct 长期保存
  • 在函数内部创建新的 Background
  • 忘记调用 cancel 导致资源泄漏
  • 用 context 传大量业务数据

工程结论:context 是控制信号,不是数据容器。


九、context 的工程定位总结

  • context 统一管理 goroutine 生命周期
  • context 是并发取消与超时的标准方式
  • context 是现代 Go 服务的基础设施

一句话总结:

没有 context 的并发代码,迟早会失控。


预告:Day 16|并发模式与实践(worker pool / fan-in / fan-out)

下一天将系统讲解:

  • worker pool 模式
  • fan-in / fan-out 模式
  • 如何限制并发度
  • 把前面所有并发工具组合成“可控并发”

发表评论