跳到主要内容

Go 上下文(context)详解

什么是上下文

在程序运行过程中,上下文(Context)是一个可供所有子程序访问的对象,包含了所有子程序所需的部分数据或功能。通过这个对象,子程序之间可以共享信息,实现协作。

context.TODOcontext.Background 的区别

context.TODOcontext.Background 都用于初始化一个上下文对象,但使用场景有所不同。

  • context.Background:最基本的上下文,通常用于 main 函数、初始化或测试代码中,或作为所有上下文的根节点。
  • context.TODO:用于尚未确定应使用哪种上下文的场景。它是一个占位符,提示开发者需要在未来确定具体的上下文类型。

Context 接口

在 Go 的 context 包中,Context 是一个接口而非结构体,定义了以下方法:

方法类型描述
Deadline() (deadline time.Time, ok bool)返回上下文被取消的截止时间。如果设置了截止时间,oktrue,否则为 false
Done() <-chan struct{}返回一个通道,当上下文被取消或截止时间到达时,该通道会被关闭。
Err() error返回上下文结束的原因,通常是 context.Canceledcontext.DeadlineExceeded
Value(key interface{}) interface{}根据键从上下文中获取对应的值,用于在上下文中传递请求范围内的数据。

Done 信号的传递机制

当调用 context.WithCancelcontext.WithDeadlinecontext.WithTimeout 函数时,会返回一个新的子上下文和一个取消函数。这个子上下文包含一个 Done 通道,当取消函数被调用时,Done 通道会被关闭。

context.WithCancel

context.WithCancel 返回一个带取消功能的上下文。当不再需要该上下文,操作完成或希望提前终止时,可以调用取消函数来取消上下文。使用该上下文的操作应检查上下文是否已被取消,并相应地停止执行。

假设有一个正在执行的 HTTP 请求,如果用户取消了这个请求,可以调用取消函数来停止后续处理。

context.WithDeadline

context.WithDeadline 创建一个带截止时间的上下文。需要提供一个父上下文和一个未来的时间点,表示希望操作在该时间点前完成。如果操作未在截止时间前完成,上下文会自动取消。

例如,需要在 5 秒内从数据库读取数据,否则视为操作失败,可以设置一个 5 秒后的截止时间。

context.WithTimeout

context.WithTimeoutcontext.WithDeadline 类似,但更加方便。无需指定具体的截止日期,只需指定一个持续时间,函数内部会计算出截止时间。这样,可以设置操作在一定时间段后自动取消。

例如,在 Web 应用中,限制某个操作最多执行 2 分钟,可以使用该函数设置超时时间。

使用场景总结

  • WithCancel:当需要能够随时取消操作时使用。
  • WithDeadline:当有一个固定的截止时间,希望操作在此之前完成时使用。
  • WithTimeout:当希望操作在一定时间段后自动取消时使用。

上下文如何在 goroutine 树中传值

上下文的 Value 方法用于传递请求范围内的值。通过上下文树传递数据,每个上下文节点可以关联一对键值对,然后在上下文树中的任何位置都可以检索这些值。

package main

import (
"context"
"fmt"
"sync"
)

func main() {
// 创建根上下文
ctx := context.Background()
// 使用 WithValue 创建携带请求 ID 的上下文
ctx = context.WithValue(ctx, "requestID", "sumingcheng")

// 处理请求的函数
processRequest(ctx)
}

func processRequest(ctx context.Context) {
// 从上下文中获取请求 ID
requestID := ctx.Value("requestID").(string)

// 启动多个 goroutine 并行处理任务
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 每个 goroutine 都可以访问相同的请求 ID
fmt.Printf("Goroutine %d 正在处理请求 %s\n", id, requestID)
}(i)
}

// 等待所有 goroutine 完成
wg.Wait()
}

Done 信号如何传递

Done 方法返回一个类型为 <-chan struct{} 的通道,当上下文被取消或截止时间到达时,该通道会被关闭。子上下文会继承父上下文的 Done 通道,当父上下文被取消时,所有子上下文的 Done 通道也会关闭,实现取消信号在 goroutine 树中的传递。

package main

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

func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
// 当上下文被取消时,Done 通道会关闭
fmt.Printf("Worker %d 接收到停止信号\n", id)
return
default:
// 模拟工作
fmt.Printf("Worker %d 正在工作...\n", id)
time.Sleep(1 * time.Second)
}
}
}

func main() {
// 创建根上下文和取消函数
ctx, cancel := context.WithCancel(context.Background())

// 启动第一个 worker
go worker(ctx, 1)

// 启动第二个 worker
go worker(ctx, 2)

// 模拟一段时间后取消操作
time.Sleep(5 * time.Second)
fmt.Println("主程序发送取消信号")
cancel() // 取消所有使用 ctx 的 goroutine

// 等待 goroutine 完成清理
time.Sleep(2 * time.Second)
}

提示 使用上下文传递取消信号,可以优雅地管理 goroutine 的生命周期,避免资源泄漏。