golang中为什么要有context,context常见的用法
为什么要用context
在软件开发中,我们经常需要在函数调用链中传递一些信息,比如请求的截止时间、取消信号等。这些信息对于整个请求的处理流程至关重要。
context 提供了一种在 Go 程序中传递请求范围的值(例如,请求ID)和取消信号的方式。
context 是什么
context 是 Go 语言标准库中的一个包,它定义了一个 Context
类型,用于在 Go 程序中传递请求范围的值、取消信号和超时信息。简单来说,它是一个键值对的集合,可以在函数调用链中传递。
如何使用 context
- 创建 Context:
context.Background()
: 创建一个新的、空的 context,通常用作根 context。context.TODO()
: 当代码中不知道应该使用哪个 context 时,可以使用这个函数。
- 取消 Context:
ctx, cancel := context.WithCancel(parentCtx)
: 创建一个可以在任何时候被取消的 context。parentCtx
是父 context。cancel()
: 调用这个函数可以取消 context,所有从这个 context 派生的子 context 也会被取消。
- 设置截止时间:
ctx, cancel := context.WithTimeout(parentCtx, timeout)
: 创建一个带有截止时间的 context。如果超时时间到了,context 会被自动取消。
- 设置超时时间:
ctx, cancel := context.WithDeadline(parentCtx, deadline)
: 创建一个带有截止时间点的 context。deadline
是一个时间点。
- 传递值:
ctx := context.WithValue(parentCtx, key, val)
: 向 context 中添加键值对。这些值可以在程序的任何地方被检索。
- 错误处理:
err := ctx.Err()
: 检查 context 是否已经取消或超时,返回错误信息。
- 值检索:
val := ctx.Value(key)
: 从 context 中检索值。
- 使用 Context:
- 在函数中,通常将 context 作为第一个参数,以支持取消操作和截止时间。
- Go 协程中的 Context 使用:
- 在启动 Go 协程时,应该传递 context 给协程,以便协程可以响应取消信号。
- 示例代码:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(3 * time.Second)
cancel()
}()
select {
case <-time.After(5 * time.Second):
fmt.Println("Done")
case <-ctx.Done():
fmt.Println("Context was canceled:", ctx.Err())
}
}
在使用 context
时,要注意以下几点:
- 不要在多个 Go 协程 中使用同一个 cancel 函数。
- 避免在 context 中存储可变状态。
- 避免在 context 中存储大的值,因为它们可能会被复制多次。
context的好处
- 取消操作:可以在请求不再需要时取消正在运行的任务。
- 超时控制:可以为请求设置超时时间,防止程序无限期等待。
- 传递请求范围的值:可以在不同的函数和 goroutine 之间传递请求相关的信息
业务场景:在线文件处理服务
在这个场景中,我们有一个在线服务,用户可以上传文件并请求处理,比如图像识别或数据分析。服务需要能够:
- 取消操作:如果用户决定不再需要处理结果,他们可以取消正在处理的任务。
- 超时控制:为了防止服务器资源被无限占用,我们为每个任务设置一个最大执行时间。
- 传递请求范围的值:我们需要在不同的服务组件之间传递用户ID、文件ID等信息,以确保任务的上下文一致性。
// 导入Go语言的包,用于程序的运行。
package main
import (
"context" // 用于处理并发的包,提供取消操作和超时处理。
"fmt" // 用于格式化I/O操作的包。
"os" // 用于操作系统功能接口的包。
"os/signal" // 用于监听操作系统信号的包。
"sync" // 用于同步原语的包,如互斥锁。
"time" // 用于时间相关操作的包。
)
// FileStatus 定义文件处理的状态结构,包含名称和描述。
type FileStatus struct {
Name string
Description string
}
// 定义文件处理状态常量,初始化为不同的状态和描述。
var (
StatusNotStarted FileStatus = FileStatus{Name: "NotStarted", Description: "未被处理"}
StatusProcessing FileStatus = FileStatus{Name: "Processing", Description: "处理中"}
StatusCanceled FileStatus = FileStatus{Name: "Canceled", Description: "已取消"}
StatusCompleted FileStatus = FileStatus{Name: "Completed", Description: "处理成功"}
)
// FileContext 结构体用于维护文件的上下文信息,包括文件ID、用户ID、状态和互斥锁。
type FileContext struct {
FileID string // 文件的唯一标识符。
UserID string // 用户的ID。
Status *FileStatus // 当前文件的状态。
mutex sync.Mutex // 互斥锁,用于同步状态更新。
}
// updateFileStatus 函数用于安全地更新文件状态,通过互斥锁确保线程安全。
func updateFileStatus(fileCtx *FileContext, newStatus *FileStatus) {
fileCtx.mutex.Lock() // 锁定互斥锁。
fileCtx.Status = newStatus // 更新状态。
fileCtx.mutex.Unlock() // 解锁互斥锁。
}
// printStatusLoop 函数是一个循环,用于定期打印文件的状态信息。
func printStatusLoop(fileCtx *FileContext, exitChan chan struct{}) {
ticker := time.NewTicker(1 * time.Second) // 创建一个定时器,每秒触发一次。
defer ticker.Stop() // 确保在函数结束时停止定时器。
for {
select {
case <-ticker.C: // 等待定时器触发。
fileCtx.mutex.Lock() // 锁定互斥锁。
fmt.Printf("用户%s 正在处理文件 %s... 当前状态: %sn", fileCtx.UserID, fileCtx.FileID, fileCtx.Status.Description) // 打印状态信息。
fileCtx.mutex.Unlock() // 解锁互斥锁。
case <-exitChan: // 等待退出信号。
return // 退出循环。
}
}
}
// processFile 函数模拟文件处理任务,接受context用于控制任务取消,FileContext保存文件状态,exitChan用于通知状态打印循环退出。
func processFile(ctx context.Context, fileCtx *FileContext, exitChan chan struct{}) {
fmt.Printf("用户%s 的文件 %s 开始处理... 当前状态: %sn", fileCtx.UserID, fileCtx.FileID, fileCtx.Status.Description) // 打印开始处理信息。
// 更新状态为正在处理,并打印初始状态。
updateFileStatus(fileCtx, &StatusProcessing)
// 启动一个goroutine来定期打印状态信息。
go func() {
for {
select {
case <-ctx.Done(): // 等待context通知完成或取消。
return // 退出goroutine。
default:
fileCtx.mutex.Lock() // 锁定互斥锁。
fmt.Printf("用户%s 正在处理文件 %s... 当前状态: %sn", fileCtx.UserID, fileCtx.FileID, fileCtx.Status.Description) // 打印状态信息。
fileCtx.mutex.Unlock() // 解锁互斥锁。
}
time.Sleep(1 * time.Second) // 休眠一秒。
}
}()
// 模拟文件处理逻辑,10秒后自动完成。
select {
case <-ctx.Done(): // 等待context通知。
if ctx.Err() == context.Canceled { // 检查是否被取消。
updateFileStatus(fileCtx, &StatusCanceled) // 更新状态为已取消。
fmt.Printf("用户%s 的文件 %s 被取消。n", fileCtx.UserID, fileCtx.FileID) // 打印取消信息。
}
close(exitChan) // 任务完成或取消,关闭exitChan。
return // 退出函数。
case <-time.After(5 * time.Second): // 等待5秒。
updateFileStatus(fileCtx, &StatusCompleted) // 更新状态为处理成功。
fmt.Printf("用户%s 的文件 %s 处理成功。n", fileCtx.UserID, fileCtx.FileID) // 打印成功信息。
close(exitChan) // 任务完成,关闭exitChan。
}
}
func main() {
// 创建文件上下文,初始化文件ID、用户ID和状态。
fileCtx := &FileContext{
FileID: "file_789",
UserID: "123456",
Status: &StatusNotStarted,
mutex: sync.Mutex{},
}
// 创建退出通道,用于通知其他goroutine退出。
exitChan := make(chan struct{})
defer close(exitChan) // 确保在main结束前关闭退出通道。
// 创建一个可以取消的context,用于控制文件处理任务的取消。
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保在main结束前取消context。
// 捕捉中断信号,用于处理用户中断操作,如Ctrl C。
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, os.Kill)
// 启动一个goroutine来监听中断信号,并在接收到信号时调用cancel函数。
go func() {
sig := <-signalChan // 阻塞等待接收到信号。
fmt.Println("接收到中断信号:", sig)
cancel() // 调用cancel来取消context。
close(exitChan) // 关闭退出通道,通知其他goroutine退出。
}()
// 启动文件处理任务,作为goroutine运行。
go processFile(ctx, fileCtx, exitChan)
// 打印状态信息的循环可以在这里启动,如果需要的话。
// 例如:
// go printStatusLoop(fileCtx, exitChan)
// 等待文件处理任务完成或被取消。
select {
case <-ctx.Done():
// 此处不需要额外处理,因为processFile已经处理了状态更新。
case <-exitChan:
// 状态打印goroutine已经停止。
}
// 打印最终状态,通过锁定互斥锁保证线程安全。
fileCtx.mutex.Lock()
fmt.Printf("文件处理最终状态:%sn", fileCtx.Status.Description)
fileCtx.mutex.Unlock()
// 清理,移除信号监听。
signal.Stop(signalChan)
// 正常退出程序。
os.Exit(0)
}