golang中为什么要有context,context常见的用法

2024-08-30 22:51:09 浏览数 (2)

golang中为什么要有context,context常见的用法

为什么要用context

在软件开发中,我们经常需要在函数调用链中传递一些信息,比如请求的截止时间、取消信号等。这些信息对于整个请求的处理流程至关重要。

context 提供了一种在 Go 程序中传递请求范围的值(例如,请求ID)和取消信号的方式。

context 是什么

context 是 Go 语言标准库中的一个包,它定义了一个 Context 类型,用于在 Go 程序中传递请求范围的值、取消信号和超时信息。简单来说,它是一个键值对的集合,可以在函数调用链中传递。

如何使用 context

  1. 创建 Context:
    • context.Background(): 创建一个新的、空的 context,通常用作根 context。
    • context.TODO(): 当代码中不知道应该使用哪个 context 时,可以使用这个函数。
  2. 取消 Context:
    • ctx, cancel := context.WithCancel(parentCtx): 创建一个可以在任何时候被取消的 context。parentCtx 是父 context。
    • cancel(): 调用这个函数可以取消 context,所有从这个 context 派生的子 context 也会被取消。
  3. 设置截止时间:
    • ctx, cancel := context.WithTimeout(parentCtx, timeout): 创建一个带有截止时间的 context。如果超时时间到了,context 会被自动取消。
  4. 设置超时时间:
    • ctx, cancel := context.WithDeadline(parentCtx, deadline): 创建一个带有截止时间点的 context。deadline 是一个时间点。
  5. 传递值:
    • ctx := context.WithValue(parentCtx, key, val): 向 context 中添加键值对。这些值可以在程序的任何地方被检索。
  6. 错误处理:
    • err := ctx.Err(): 检查 context 是否已经取消或超时,返回错误信息。
  7. 值检索:
    • val := ctx.Value(key): 从 context 中检索值。
  8. 使用 Context:
    • 在函数中,通常将 context 作为第一个参数,以支持取消操作和截止时间。
  9. Go 协程中的 Context 使用:
    • 在启动 Go 协程时,应该传递 context 给协程,以便协程可以响应取消信号。
  10. 示例代码:
代码语言:go复制
    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的好处

  1. 取消操作:可以在请求不再需要时取消正在运行的任务。
  2. 超时控制:可以为请求设置超时时间,防止程序无限期等待。
  3. 传递请求范围的值:可以在不同的函数和 goroutine 之间传递请求相关的信息

业务场景:在线文件处理服务

在这个场景中,我们有一个在线服务,用户可以上传文件并请求处理,比如图像识别或数据分析。服务需要能够:

  1. 取消操作:如果用户决定不再需要处理结果,他们可以取消正在处理的任务。
  2. 超时控制:为了防止服务器资源被无限占用,我们为每个任务设置一个最大执行时间。
  3. 传递请求范围的值:我们需要在不同的服务组件之间传递用户ID、文件ID等信息,以确保任务的上下文一致性。
代码语言:c复制
// 导入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)
}
go

0 人点赞