在 Go 语言中,context 包提供了一种在函数之间传递请求作用域的方法。它通常用于跨 API 边界传递取消信号、超时值、截止时间以及请求范围的数据。
一、Context 接口
context.Context
接口是 Go 语言中用于管理请求作用域的重要接口之一。它定义了一组方法,用于传递取消信号、超时值、截止时间以及请求范围的数据。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
-
Deadline() (deadline time.Time, ok bool)
:
返回上下文的截止时间(deadline),如果没有设置截止时间,则 ok 为 false。可以用于检查上下文是否有截止时间,并获取该截止时间。 -
Done() <-chan struct{}
:
返回一个通道Channel(<-chan struct{}
),当上下文被取消或超时时,该通道会被关闭。可以通过监听这个通道来接收取消信号,执行相应的清理或中止操作。 -
Err() error
:
返回上下文取消的原因,如果上下文没有被取消,则返回nil
。可以用于检查上下文是否已被取消,并获取取消的具体原因。 -
Value(key interface{}) interface{}
:
返回与键关联的值,如果键不存在则返回nil
。可以用于在请求范围内传递键值对,比如传递请求标识、用户认证信息等。
context包主要提供了两种方式创建context:
context.Backgroud()
:是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO()
:只在不确定应该使用哪种上下文时使用。
这两个函数其实只是互为别名,没有差别。
二、Contex的With系列函数
2.1 WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
context.WithCancel
函数用于创建一个带有取消功能的上下文(Context)。当调用返回的CancelFunc
时,所有通过该上下文启动的goroutine都会收到一个取消信号。这通常用于需要响应外部请求取消的场景,比如HTTP长轮询或者WebSocket连接。
实际使用context.WithCancel时,你需要确保正确地处理了取消信号,并且在适当的时候调用了cancel函数,以避免资源泄漏或不必要的CPU消耗
package main
import (
"context"
"fmt"
"time"
)
// 模拟一个长时间运行的任务
func longRunningTask(ctx context.Context, name string) {
for i := 0; ; i++ {
select {
case <-ctx.Done(): // 检查上下文是否被取消
return // 如果被取消,立即返回
case <-time.After(1 * time.Second): // 每秒输出一次进度
fmt.Printf("%s: %d
", name, i)
}
}
}
func main() {
// 创建一个新的带取消功能的上下文
ctx, cancel := context.WithCancel(context.Background())
// 使用新创建的上下文启动任务
go longRunningTask(ctx, "task")
// 主线程等待5秒后取消上下文
time.Sleep(5 * time.Second)
cancel() // 发送取消信号
// 为了演示,我们再等2秒钟以确保任务已经接收到了取消信号并且退出
time.Sleep(2 * time.Second)
}
2.2 WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
WithDeadline
函数用于创建一个带有截止时间的新的Context。这个截止时间可以用来限制操作的执行时间,当超过截止时间时,可以自动取消Context
package main
import (
"context"
"fmt"
"time"
)
func main() {
parentCtx := context.Background()
deadline := time.Now().Add(time.Millisecond * 50)
// 创建一个带有截止时间的新context,设置截止时间为当前时间的50毫秒之后
ctx, cancel := context.WithDeadline(parentCtx, deadline)
// 确保在main函数返回前取消context以释放资源
defer cancel()
// 模拟一个需要执行的操作,比如等待100毫秒
go func() {
select {
case <-time.After(100 * time.Millisecond):
fmt.Println("操作完成")
case <-ctx.Done():
fmt.Println("操作超时")
}
}()
// 等待一段时间,以便观察操作是否在截止时间之前完成
time.Sleep(time.Millisecond * 200)
}
我们设置截止时间为当前时间的50毫秒之后。接着,我们启动一个goroutine
来模拟一个长时间运行的操作,通过监听ctx.Done()
来判断操作是否在截止时间之前完成。在主函数结束前,我们通过defer
语句调用cancel
函数来确保及时释放资源。
2.3 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout
是一种用于设置操作执行的超时时间的机制,它允许你指定一个时间段,在这个时间段内如果操作没有完成,就会抛出一个超时异常或执行一个指定的回调函数。
package main
import (
"context"
"fmt"
"time"
)
func myFunction(ctx context.Context, duration time.Duration) {
select {
case <-time.After(duration):
fmt.Println("操作成功")
case <- ctx.Done():
fmt.Println("操作失败")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
myFunction(ctx, 2*time.Second)
}
2.4 WithValue
func WithValue(parent Context, key, val interface{}) Context
WithValue
用于创建一个带有额外值的新上下文,这个值可以在上下文中进行传递。通常情况下,WithValue方法用于将一些请求范围的值(例如请求ID、用户信息等)与上下文关联起来,以便在整个请求处理过程中进行传递和访问。
需要注意的是,在使用context.WithValue方法时,应避免直接使用内置类型string作为键,因为这可能导致潜在的键冲突问题。考虑到context.WithValue方法接受interface{}类型的键和值,如果在不同的地方使用了相同的字符串作为键,那么就会导致冲突。为了避免这种情况,建议定义自己的类型作为键
package main
import (
"context"
"fmt"
)
// 定义一个自定义类型作为键
type contextKey string
func processRequest(ctx context.Context) {
// 从上下文中获取请求ID
reqID := ctx.Value(contextKey("requestID")).(int)
// 模拟处理请求的操作
fmt.Printf("Processing request with ID %d\n", reqID)
}
func main() {
// 创建一个带有请求ID的上下文
ctx := context.WithValue(context.Background(), contextKey("requestID"), 12345)
// 处理请求
processRequest(ctx)
}