context 库提供了一种机制,用于跨 API 边界传递请求范围的值、取消信号以及请求超时等信息。通过使用 context,我们可以优雅地管理应用程序中的并发和 goroutine,并避免可能会导致死锁的竞争条件。
为什么需要 context
在实际的应用中,我们通常会有一些需要处理请求的操作,例如发送请求、访问数据库、调用其他 API 等。这些操作往往会使用 goroutine 并发执行,因此在进行这些操作时,我们需要协调它们之间的同步和协作。
在一个 HTTP 请求处理流程中,多个 goroutine 会在相同的上下文中协同工作,它们之间可能需要相互传递一些信息,例如:请求 ID、认证信息、日志记录器等。使用 context 可以帮助我们实现这些功能,并且在某些情况下,我们还可以使用 context 来管理超时和取消操作,从而提高应用程序的可靠性和稳定性。
context的组成
context 库由三个主要的组成部分构成:
context.Context 接口:上下文接口定义了一组标准方法,包括获取上下文的值、取消上下文和检查是否已取消上下文。它是上下文管理的核心接口,由其他上下文相关的函数和方法使用。
context.WithValue() 函数:这个函数可以基于原来的上下文创建一个新的上下文,同时在新上下文中添加一个键值对。它返回一个新的上下文实例,该实例包含在原上下文中存在的所有信息,同时还添加了新键值对。
context.Background() 和 context.TODO() 函数:context.Background() 函数返回一个空上下文,没有任何值和取消信号;context.TODO() 函数返回一个空上下文,没有任何值和取消信号,它通常被用来表示当前上下文不知道应该使用哪个上下文。
context 库还提供了一些其他的工具函数,如 context.WithCancel()、context.WithDeadline()、context.WithTimeout() 等,这些函数都是基于 context.Context 接口的标准方法和 context.WithValue() 函数构建而成的,可以帮助我们更方便地管理和使用上下文信息
context 的基本用法
context 可以理解为一个包含了请求范围的值和取消信号的容器。下面我们来看一下 context 的基本使用方式。
首先,我们需要在请求处理函数中创建一个 context 对象,该对象通常由外部传递进来,例如:
unchandler(ctx context.Context, req *http.Request) {
// ...
}
然后,我们可以使用 context 对象中的方法来获取请求范围的值和取消信号,例如:
unchandler(ctx context.Context, req *http.Request) {
// 获取请求 ID
reqID := ctx.Value("reqID").(string)
// 监听取消信号select {
case <-ctx.Done():
// 请求已经取消default:
// 请求未取消
}
}
在这个例子中,我们使用 ctx.Value() 方法来获取请求 ID,使用 ctx.Done() 方法来监听取消信号。当 ctx 对象中的 Done() 方法返回时,表示该请求已经被取消,我们可以通过读取 Err() 方法的返回值来确定请求被取消的原因。
接下来,我们可以使用 context 对象来派生新的 context 对象,例如:
unchandler(ctx context.Context, req *http.Request) {
// 派生新的 context 对象
ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// ...
}
在这个例子中,我们使用 context.WithTimeout() 方法来创建一个新的 context 对象,并指定该请求的处理时间不能超过 5 秒钟。在处理函数结束时,我们需要调用 cancel() 方法来释放相关的资源。
context 的实际应用
超时控制
package main
import (
"context""fmt""time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("waited for 1 second")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
上述代码中,我们使用 context.WithTimeout 创建了一个上下文,并设置了超时时间为 2 秒。在 select 语句中,我们先等待 1 秒钟,然后再监听上下文的 Done 信号。如果在 2 秒钟内没有收到 Done 信号,说明请求处理成功;如果在 2 秒钟内收到了 Done 信号,说明请求已经超时,我们可以输出超时信息。
取消操作
package main
import (
"context""fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
gofunc() {
select {
case <-time.After(3 * time.Second):
fmt.Println("work done")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}()
time.Sleep(1 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
上述代码中,我们使用 context.WithCancel 创建了一个上下文,并通过 cancel 函数提供了取消请求的功能。在 goroutine 中,我们使用 select 监听上下文的 Done 信号,如果在取消请求前收到了信号,说明请求已经取消。否则,我们会等待 3 秒钟后输出 "work done" 信息
上下文传递
package main
import (
"context"
"fmt"
)
type key int
const requestIDKey key = 0
func main() {
ctx := context.Background()
// 传递请求 ID 到上下文中
ctx = context.WithValue(ctx, requestIDKey, "123")
// 启动一个 goroutine,使用 context 进行传递
go func(ctx context.Context) {
requestID := ctx.Value(requestIDKey).(string)
fmt.Println("request ID:", requestID)
}(ctx)
// 等待 goroutine 执行完毕
time.Sleep(1 * time.Second)
}