Go的Context包

背景

context 包是标准库中一个非常重要的组件,用于在不同 Goroutine 之间传递取消信号、超时警告和其他请求范围的值。它主要用于控制并发任务的生命周期,尤其是在复杂的网络服务和多层调用栈中。context 包的使用能够帮助开发人员写出更加清晰、健壮和易于维护的并发代码。下面介绍 context 包的基本用法和设计理念。

为什么需要使用context

在并发程序中使用 Context 主要有两个目的:一是用来取消操作,二是用来传递请求范围的数据。

  • 取消操作:当一个 Context 被取消时,所有基于该 Context 的操作都应该被取消。这是通过检查 ctx.Done() 返回的通道是否被关闭来实现的。如果该通道被关闭,表示 Context 已经被取消,相应的操作也应该停止。
  • 传递数据:通过 context.WithValue 函数,可以将请求范围的值与 Context 相关联。这可以用来传递如请求ID、认证令牌等信息。

常见用法

创建context

context 包通过 Context 接口提供功能。能直接创建一个 Context 实例,而是需要通过包提供的几个函数来获取:

  • context.Background():返回一个空的 Context。这个 Context 通常被用作整个 Context 树的根节点。
  • context.TODO():当你不确定应该使用哪个 Context 或者还没有可用的 Context 时,可以使用这个函数。它的行为和 context.Background() 一样,但在代码中使用 TODO 可以帮助你记住需要回过头来确定正确的 Context 来使用。

派生 Context

context 包允许从一个现有的 Context 派生出新的 Context 实例。这是通过以下两个主要的函数实现的:

  • context.WithCancel(parent Context):创建一个可被取消的 Context。返回派生的 Context 和一个取消函数,调用这个取消函数可以取消这个 Context 及其所有派生的 Context
  • context.WithDeadline(parent Context, deadline time.Time):创建一个有截止时间的 Context。当到达截止时间或者手动调用返回的取消函数时,该 Context 会被取消。
  • context.WithTimeout(parent Context, timeout time.Duration):它是 context.WithDeadline 的简化版,让你可以直接设置一个超时时间。

创建并取消 Context

package main

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

func operation(ctx context.Context) {
select {
case <-time.After(500 * time.Millisecond): // 模拟耗时操作
fmt.Println("operation completed")
case <-ctx.Done():
fmt.Println("operation canceled")
}
}

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

go operation(ctx)

time.Sleep(100 * time.Millisecond) // 在取消前等待一段时间
cancel() // 取消操作

// 给足够的时间来查看 operation 如何响应取消信号
time.Sleep(1 * time.Second)
}

使用超时的 Context

package main

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

func operation(ctx context.Context) {
select {
case <-time.After(1 * time.Second): // 模拟耗时操作
fmt.Println("operation completed")
case <-ctx.Done():
fmt.Println("operation canceled")
}
}

func main() {
// 创建一个将在 500 毫秒后自动取消的 Context
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // 在 main 函数结束时调用 cancel,以确保所有资源都被释放

go operation(ctx)

// 给足够的时间来查看 operation 如何响应超时信号
time.Sleep(1 * time.Second)
}

使用 Context 传递值

package main

import (
"context"
"fmt"
)

func process(ctx context.Context) {
// 从 Context 中取值
value := ctx.Value("key").(string)
fmt.Println("processing:", value)
}

func main() {
// 创建一个携带值的 Context
ctx := context.WithValue(context.Background(), "key", "my value")

process(ctx)
}