Go 上下文(context)详解
什么是上下文
在程序运行过程中,上下文(Context)是一个可供所有子程序访问的对象,包含了所有子程序所需的部分数据或功能。通过这个对象,子程序之间可以共享信息,实现协作。
context.TODO
和 context.Background
的区别
context.TODO
和 context.Background
都用于初始化一个上下文对象,但使用场景有所不同。
context.Background
:最基本的上下文,通常用于main
函数、初始化或测试代码中,或作为所有上下文的根节点。context.TODO
:用于尚未确定应使用哪种上下文的场景。它是一个占位符,提示开发者需要在未来确定具体的上下文类型。
Context 接口
在 Go 的 context
包中,Context
是一个接口而非结构体,定义了以下方法:
方法 | 类型 | 描述 |
---|---|---|
Deadline | () (deadline time.Time, ok bool) | 返回上下文被取消的截止时间。如果设置了截止时间,ok 为 true ,否则为 false 。 |
Done | () <-chan struct{} | 返回一个通道,当上下文被取消或截止时间到达时,该通道会被关闭。 |
Err | () error | 返回上下文结束的原因,通常是 context.Canceled 或 context.DeadlineExceeded 。 |
Value | (key interface{}) interface{} | 根据键从上下文中获取对应的值,用于在上下文中传递请求范围内的数据。 |
Done
信号的传递机制
当调用 context.WithCancel
、context.WithDeadline
或 context.WithTimeout
函数时,会返回一个新的子上下文和一个取消函数。这个子上下文包含一个 Done
通道,当取消函数被调用时,Done
通道会被关闭。
context.WithCancel
context.WithCancel
返回一个带取消功能的上下文。当不再需要该上下文,操作完成或希望提前终止时,可以调用取消函数来取消上下文。使用该上下文的操作应检查上下文是否已被取消,并相应地停止执行。
假设有一个正在执行的 HTTP 请求,如果用户取消了这个请求,可以调用取消函数来停止后续处理。
context.WithDeadline
context.WithDeadline
创建一个带截止时间的上下文。需要提供一个父上下文和一个未来的时间点,表示希望操作在该时间点前完成。如果操作未在截止时间前完成,上下文会自动取消。
例如,需要在 5 秒内从数据库读取数据,否则视为操作失败,可以设置一个 5 秒后的截止时间。
context.WithTimeout
context.WithTimeout
与 context.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 的生命周期,避免资源泄漏。