Sirius
Sirius

目录

golang context 原理

  1. 取消传播 (Cancellation Propagation):

    • 这是 context 最核心和最常用的功能。当一个操作因为某种原因(例如,用户取消了请求、上游服务超时或出错、父操作不再需要结果)需要被终止时,可以使用 context 来通知所有相关的、为此操作派生出来的 Goroutine 停止它们的工作。
    • 这有助于避免不必要的资源消耗(如 CPU、内存、网络连接),并及时释放资源。例如,一个 HTTP 请求可能触发多个后台 Goroutine 去查询数据库、调用其他微服务等。如果客户端断开了连接,服务器应该能够取消这些后台任务。
  2. 超时控制 (Timeout/Deadline Management):

    • context 允许你为一个操作或一系列操作设置一个截止时间点 (Deadline) 或一个超时时长 (Timeout)。
    • 如果操作在指定的时间内未能完成,context 会自动发出取消信号。
    • 这对于防止操作无限期阻塞、保证系统的响应性和可靠性至关重要。例如,在调用外部 API 时,设置一个超时,如果对方服务长时间未响应,则主动放弃并返回错误。
  3. 传递请求作用域的值 (Request-scoped Values):

    • context 可以携带键值对数据,这些数据可以在一个请求的处理链中(跨越多个函数调用和不同的 Goroutine)安全地传递。
    • 这些值通常是与特定请求相关的信息,例如追踪 ID (trace ID)、用户身份信息、授权令牌等。
    • 重要提示: context.WithValue 不应该被滥用。它主要用于传递贯穿整个调用链的、与请求本身相关的元数据,而不是用来替代函数的可选参数。函数的显式参数通常是更清晰的选择。
  4. 控制 Goroutine 的生命周期:

    • 通过将 context 传递给新启动的 Goroutine,父 Goroutine 可以有效地控制子 Goroutine 的生命周期。当父 context 被取消时,所有衍生的子 context 也会被取消,子 Goroutine 可以监听到这个信号并优雅地退出。

context 包的核心是 Context 接口,它定义了所有上下文类型要实现的方法:

type Context interface {
        // Deadline 返回一个时间点,当到达这个时间点时,context 会被自动取消。
    // 如果没有设置 deadline,ok 会是 false。
    Deadline() (deadline time.Time, ok bool)

    // Done 返回一个 channel。当此 context 被取消或超时时,该 channel 会被关闭。
    // 如果 context 不能被取消,Done 可能返回 nil。
    // Done 通常在 select 语句中使用,以监听取消信号。
    Done() <-chan struct{}

    // Err 返回 context 被取消的原因。
    // 如果 Done channel尚未关闭,Err 返回 nil。
    // 如果 Done channel已关闭,Err 返回一个非 nil 的错误:
    //  - Canceled: context 是通过调用 cancel 函数取消的。
    //  - DeadlineExceeded: context 是因为截止时间到达而被取消的。
    Err() error

    // Value 返回与此 context关联的键key对应的值,如果不存在则返回 nil。
    // key 应该是不易冲突的类型,通常是自定义的非导出类型。
    // 主要用于在请求范围内传递数据。
    Value(key any) any
}
  • Done() Channel (核心取消机制):

    • 每个可被取消的 context(即通过 WithCancelWithDeadlineWithTimeout 创建的)都会有一个 Done() 方法,返回一个只读的 channel (<-chan struct{})。
    • 当该 context 被取消或其截止时间到达时,这个 Done() channel 会被关闭 (closed)。
    • Goroutine 可以通过 select 语句监听这个 Done() channel。一旦 channel 关闭,Goroutine 就知道它应该停止当前的工作、进行清理并退出。
    select {
    case <-ctx.Done():
        // Context 被取消了,执行清理操作
        log.Println("Operation cancelled:", ctx.Err())
        return ctx.Err()
    case result := <-doSomeWork():
        // 工作正常完成
        return result
    }
  • Err() 方法 (取消原因):

    • 一旦 Done() channel 被关闭,Err() 方法就会返回一个非 nil 的错误,用来说明 context 被取消的原因:
      • context.Canceled: 如果 context 是通过显式调用其 cancel 函数而被取消的。
      • context.DeadlineExceeded: 如果 context 是因为其设置的截止时间已过或超时而被取消的。

Context 的实例可以形成一个树状结构。除了最顶层的 BackgroundTODO 上下文,每个上下文都有一个父上下文。这种父子关系是实现取消信号传播的关键。

核心规则:当一个父上下文被取消时,它的所有子上下文也会被立即取消。

这个机制是通过 cancelCtx 类型和 propagateCancel 函数实现的。

  • cancelCtx: 这是可取消上下文的核心实现。它包含:

    • 一个指向父 Context 的引用。
    • 一个 done channel,在取消时关闭。
    • 一个 children map,用于存储所有可取消的子上下文。
    • 一个 err 字段,用于存储取消原因。
  • propagateCancel 函数: 当创建一个新的可取消上下文(如 WithCancel)时,这个函数会将新的子上下文“附加”到其父上下文上。

    1. 高效路径:如果父上下文也是一个 cancelCtx,子上下文会被直接添加到父上下文的 children map 中。
    2. 兼容路径:如果父上下文是自定义类型,context 会启动一个新的 goroutine。这个 goroutine 会监听父上下文的 Done() channel。一旦父上下文被取消,它就会调用子上下文的 cancel 方法。
  • cancel() 方法: 当一个 cancelCtxcancel() 方法被调用时,它会:

    1. 关闭自己的 done channel。
    2. 设置自己的 err 字段。
    3. 遍历 children map,并递归地调用所有子上下文的 cancel() 方法,从而将取消信号传播下去。
    4. 将自己从父上下文的 children map 中移除,以便垃圾回收。

对于value context 是用于补足协程没有的ThreadLocal

  • 值继承: Value 方法在查找值时,如果当前 context 没有存储对应的键,它会递归地向其父 context 查找。

context 包提供了几种具体的 Context 实现,它们通过不同的函数创建:

  1. emptyCtx (Background, TODO)

    • context.Background()context.TODO() 返回的都是 emptyCtx 的实例。
    • 它是所有上下文树的根节点。
    • 它永远不会被取消,没有截止日期,也不携带任何值。它的 Done() 方法总是返回 nil
  2. cancelCtx (WithCancel, WithCancelCause)

    • WithCancel(parent) 会创建一个 cancelCtx
    • 它嵌入了父上下文,并实现了上面描述的取消传播逻辑。
    • 它返回一个新的 Context 和一个 CancelFunc。调用 CancelFunc 会触发取消操作。
  3. timerCtx (WithDeadline, WithTimeout)

    • WithDeadline(parent, d)WithTimeout(parent, t) 会创建一个 timerCtx
    • timerCtx 内嵌了一个 cancelCtx,所以它具备所有可取消上下文的功能。
    • 此外,它还包含一个 deadline 时间和一个 *time.Timer
    • 在创建时,它会启动一个定时器 (time.AfterFunc)。当到达 deadline 时,定时器会自动调用 cancel() 方法,从而触发超时取消。
    • 如果在超时前手动调用了 cancel 函数,它会停止内部的定时器以释放资源。
  4. valueCtx (WithValue)

    • WithValue(parent, key, val) 会创建一个 valueCtx
    • valueCtx 内嵌了父上下文,并存储了一个键值对。
    • 当调用 Value(key) 方法时:
      • 如果 valueCtx 存储的键与要查找的键匹配,就返回对应的值。
      • 否则,它会递归地调用父上下文的 Value(key) 方法,沿着上下文树向上查找,直到找到匹配的键或到达根节点。
    • 这形成了一个类似链表的结构来存储和检索值。
  • 创建函数
    • context.Background():

      • 返回一个空的 Context,它是所有 context 树的根节点。
      • 它永远不会被取消,没有值,也没有截止时间。
      • 通常用在主函数、初始化代码、测试代码中,或者作为顶级请求的起始 Context
    • context.TODO():

      • 也返回一个空的 Context,类似于 Background()
      • 当你不确定应该使用哪个 Context,或者当前函数将来可能会更新以接收一个 Context 参数时,可以使用 TODO() 作为占位符。它表明相关的 context 还没有确定或者还没有实现。
      • 静态分析工具可能会提示你将 TODO() 替换为更具体的 context
    • context.WithCancel(parent Context) (ctx Context, cancel CancelFunc):

      • 创建一个新的 Context (ctx),它是 parent 的子节点。
      • 同时返回一个 CancelFunc 类型的函数 cancel
      • 调用这个 cancel 函数会关闭 ctx.Done() channel,从而取消 ctx 及其所有派生出来的子 context
      • NOTE: cancel 函数必须被调用(通常通过 defer cancel()),以释放与该 context 相关的资源,即使操作正常完成。否则可能导致 context 树的内存泄漏。
    • context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc):

      • 创建一个新的 Context (ctx),它会在指定的时间点 d 到达时自动取消,或者当其 parent 被取消时,或者当返回的 cancel 函数被调用时。
      • 内部通常会启动一个定时器,在时间点 d 到达时调用 cancel 函数。
      • 同样需要调用返回的 cancel 函数。
    • context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):

      • 这是 WithDeadline 的一个便捷包装。它接受一个持续时间 timeout
      • WithTimeout(parent, timeout) 等价于 WithDeadline(parent, time.Now().Add(timeout))
      • 同样需要调用返回的 cancel 函数。
    • context.WithValue(parent Context, key, val any) Context:

      • 创建一个新的 Context (ctx),它携带了提供的键值对 (key, val)。
      • key 通常是一个自定义的、不可导出的类型(例如 type myKey string),以避免不同包之间的键名冲突。
      • 当调用 ctx.Value(someKey) 时,它会首先检查 ctx自身是否存储了 someKey。如果没有,它会递归地调用 parent.Value(someKey),直到找到值或者到达树的根部。