context.Context 是 Go 语言中管理协程生命周期和传递信息的核心工具,其主要作用如下:

  1. 超时退出:例如在处理 HTTP 请求时,可以在超时或取消时及时终止对应的协程。
  2. 传递元信息:通过 Context 的 Value 方法,可以携带与操作相关的元数据(如请求 ID、用户身份等),并在各层级 API 间传递。
  3. 传递取消信号:通过 Done() 通道获取取消信号来控制协程退出

context.Context 实质上只是一个接口,类型如下:

type Context interface {
    // 返回 Context 是否有截止时间,以及截止时间的具体值
    Deadline() (deadline time.Time, ok bool)

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

    // 返回 Context 被取消的具体原因
    Err() error

    // 从 Context 中获取一个与 key 相关联的值
    Value(key any) any
}

实现该接口的核心类型

  1. context.emptyCtx

     type emptyCtx struct{}
    • 描述:空的上下文,所有方法都返回默认值,主要用于生成根节点
    • 特性:

      • 没有超时
      • 没有取消机制
      • 不携带任何值
    • 生成方式:

      • context.Background()
      • context.TODO()
      • 说明:

        • 两者底层都是 context.emptyCtx 的实例,功能没有区别,仅语义不同
        • Background 通常用于根节点上下文
        • TODO 用于暂时未定的上下文场景
    • 使用场景:作为上下文树的根节点,或作为占位符


  2. context.cancelCtx

     type cancelCtx struct {
       Context
    
       mu       sync.Mutex            // protects following fields
       done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
       children map[canceler]struct{} // set to nil by the first cancel call
       err      error                 // set to non-nil by the first cancel call
       cause    error                 // set to non-nil by the first cancel call
     }
    • 描述:提供取消功能的上下文,通过嵌套父上下文,监听取消信号
    • 特性:

      • 提供 Done 通道,通知取消事件
      • 父上下文取消时,子上下文也会被取消
    • 生成方式:

      • context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
      • context.WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
      • 说明:

        • 返回一个 cancelCtx 类型的实例和一个取消函数。
        • 调用取消函数时,会触发 Done 通道,并通知所有派生的子上下文
        • WithCancelCause 的取消函数可以设置附加的错误
    • 使用场景:需要手动控制操作取消的场景


  3. context.timerCtx

     type timerCtx struct {
       cancelCtx
       timer *time.Timer // Under cancelCtx.mu.
    
       deadline time.Time
     }
    • 描述:带有超时或截止时间的上下文。
    • 特性:

      • 包含一个定时器,到期后会自动取消上下文
      • 可以嵌套父上下文的取消逻辑。
    • 生成方式:

      • context.WithTimeout(parent Context, timeout time.Duration)
      • context.WithDeadline(parent Context, deadline time.Time)
      • WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
      • WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
      • 说明:

        • WithTimeout 基于相对时间生成超时上下文
        • WithDeadline 基于绝对时间生成超时上下文
        • 超时后,Done 通道会触发,标记上下文已取消
    • 使用场景:处理需要设定超时时间的操作


  4. context.valueCtx

    type valueCtx struct {
      Context
      key, val any
    }
    • 描述:携带键值对的上下文
    • 特性:

      • 提供键值对存储功能
      • 数据是只读的,不允许修改
    • 生成方式:

      • context.WithValue(parent Context, key, val any) Context
      • 说明:

        • 生成一个 valueCtx 实例,携带指定的键值对。
        • 适用于在上下文中传递特定信息,例如请求 ID、用户权限等。
    • 使用场景:在跨 API 边界传递数据,例如用户身份、请求 ID 等
  5. context.withoutCancelCtx

     type withoutCancelCtx struct {
       c Context
     }
    • 描述:从父 Context 派生的上下文,但该上下文不会受到父上下文的取消或超时的影响
    • 特性:

      • Done() 方法始终返回 nil,表示上下文永远不会被取消。
      • 仍然可以通过 ValueDeadline 方法访问父上下文的值和超时时间(如果有)
    • 生成方式:

      • context.WithoutCancel(parent Context) Context
      • 说明:

        • 生成一个 withoutCancelCtx 实例,用于需要持续存在或不受父上下文生命周期影响
    • 使用场景:需要长时间运行的操作、避免误取消

应用场景实例

控制子协程退出
func main() {
    ctx, cancelFunc := context.WithCancel(context.Background())
    go worker(ctx)
    time.Sleep(2 * time.Second)
    fmt.Println("主业务异常,通知子协程退出....")
    cancelFunc()
    time.Sleep(1 * time.Second)
}

func worker(ctx context.Context) {
    n := 0
    for {
        n++
        select {
        case <-ctx.Done():
            fmt.Println("噢,卖糕的,我死了.....")
            return
        default:
            fmt.Println("嘿嘿,处理业务....:", n)
            time.Sleep(time.Second)
        }
    }
}

执行输出

go run main.go
嘿嘿,处理业务....: 1
嘿嘿,处理业务....: 2
主业务异常,通知子协程退出....
噢,卖糕的,我死了.....
超时退出
func main() {
    ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*3)
    defer cancelFunc()

    go worker(ctx)

    // 等待上下文的完成或超时
    <-ctx.Done()

    // 检查上下文的错误
    if err := ctx.Err(); err != nil {
        // 输出超时错误
        fmt.Println("超时错误:", err)
    } else {
        fmt.Println("任务完美完成....")
    }
}

func worker(ctx context.Context) {
    // 模拟任务运行并定期检查上下文是否被取消
    for i := 0; i < 5; i++ {
        // 每秒执行一次任务
        select {
        case <-ctx.Done():
            // 如果上下文被取消(超时或其他原因),退出任务
            fmt.Println("任务超时,退出....")
            return
        default:
            // 模拟任务的执行
            fmt.Printf("正常处理业务 %d\n", i+1)
            time.Sleep(1 * time.Second) // 模拟任务执行的延迟
        }
    }

    // 如果没有超时,任务成功完成
    fmt.Println("任务成功完成")
}

执行输出:

 go run main.go
正常处理业务 1
正常处理业务 2
正常处理业务 3
超时错误: context deadline exceeded
传递值

值传递我们一般在api入口处,传递一些全局的值,比如请求id,日志id,用户id等,整个生命周期保持一直的值

func main() {
    ctx := context.WithValue(context.Background(), "request_id", "日志id")
    ctx = context.WithValue(ctx, "user_id", "666")
    go worker(ctx)
    time.Sleep(1 * time.Second)
}

func worker(ctx context.Context) {
    go worker1(ctx)
    fmt.Println("worker:request_id", ctx.Value("request_id"))
    fmt.Println("worker:user_id", ctx.Value("user_id"))
}

func worker1(ctx context.Context) {
    fmt.Println("worker1:request_id", ctx.Value("request_id"))
    fmt.Println("worker1:user_id", ctx.Value("user_id"))
}
go run main.go
worker:request_id 日志id
worker:user_id 666       
worker1:request_id 日志id
worker1:user_id 666

使用注意

  1. WithValue 传递值不应该被滥用,局部变量不应该使用它来存储
  2. 取消函数不要在循环的使用 defer cancelFunc() 来延迟调用,应及时手动调用,防止内存泄漏

一夕烟云
1 声望1 粉丝