一、起源
context
包最初由Go语言的核心团队提出,并于Go 1.7版本正式引入。其设计灵感来源于Google内部的实践,旨在解决长期运行的并发任务和服务之间如何协调取消信号和超时控制的问题。在早期的Go语言编程中,开发者通常使用全局变量或者依赖参数传递来实现上下文管理,这种方式在多层嵌套和并发执行时,容易出现代码可读性差、维护困难等问题。因此,Go语言的设计者决定创建一个标准化的解决方案,context
应运而生。
二、主要作用
context
的主要作用是在并发操作中传递控制信号,特别是在涉及多个goroutine的操作时,它提供了以下几个核心功能:
-
取消信号(Cancellation):通过
context.CancelFunc
,Go语言允许父任务通知其子任务取消操作,这对于清理资源、停止不再需要的任务、避免内存泄漏等至关重要。 -
超时控制(Timeout/Deadline):通过
context.WithTimeout
或context.WithDeadline
,开发者可以设置任务的最大执行时间,当超时到达时,context
会自动触发取消信号,帮助避免长时间挂起的操作。 -
跨goroutine的共享数据(Request-scoped Data):
context
允许在多个goroutine之间传递元数据,特别是在一个请求生命周期内共享的值,这对于记录日志、追踪请求ID或传递认证信息等非常有用。 -
避免资源泄漏:通过控制超时和取消操作,
context
有助于及时清理资源,避免因未取消的操作或长期挂起的goroutine造成系统负担。
三、使用方式
1. 引入 context
包
在 Go 中使用 context
,首先需要引入 context
包:
import "context"
2. 创建一个 context
context
的创建通常通过 context.Background()
或 context.TODO()
方法来完成。
context.Background()
:一般用作主程序的顶层context
,通常在主函数中使用。context.TODO()
:当你不确定该使用什么context
时,可以使用context.TODO()
。通常用于暂时的占位符。
示例:
ctx := context.Background()
3. 创建具有取消功能的 context
context
包提供了 WithCancel
、WithTimeout
和 WithDeadline
等函数来创建具有特定功能的 context
。
3.1 使用 context.WithCancel
context.WithCancel
用于创建一个可以手动取消的 context
。它返回一个子 context
和一个取消函数 CancelFunc
,你可以调用取消函数来取消该 context
,从而通知相关的 goroutine 停止执行。
示例:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保在函数结束时取消
// 假设在 goroutine 中运行的任务
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务被取消:", ctx.Err())
}
}(ctx)
// 取消操作
time.Sleep(2 * time.Second) // 模拟一段时间后取消
cancel()
3.2 使用 context.WithTimeout
context.WithTimeout
用于设置一个超时期限,当超时到达时,会自动取消该 context
。
示例:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 确保在函数结束时取消
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务超时:", ctx.Err())
}
}(ctx)
在这个例子中,任务将在 2 秒后被取消,因为超时设置为 2 秒。
3.3 使用 context.WithDeadline
context.WithDeadline
用于创建一个具有特定截止时间的 context
。当达到截止时间时,context
会自动被取消。
示例:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel() // 确保在函数结束时取消
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务到期:", ctx.Err())
}
}(ctx)
4. 传递 context
给子 goroutine
通常,context
会在父 goroutine 中创建,并传递给子 goroutine。这样,子 goroutine 可以根据父 goroutine 传递的 context
来判断任务是否被取消、是否超时,或者共享一些数据。
示例:
func doTask(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务被取消:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go doTask(ctx)
time.Sleep(1 * time.Second) // 等待一段时间
cancel() // 取消任务
}
5. 在 context
中传递数据
context
允许在多个 goroutine 之间传递请求相关的数据(例如,请求 ID 或认证信息)。可以通过 context.WithValue
创建一个新的 context
,并将键值对数据存储在 context
中。
注意:不要用 context
传递大数据或敏感信息,因为它的主要目的是携带跨 API 边界和跨 goroutine 的请求级别的数据。
示例:
type key string
const UserIDKey key = "userID"
func doTask(ctx context.Context) {
userID := ctx.Value(UserIDKey)
fmt.Println("UserID:", userID)
}
func main() {
ctx := context.Background()
// 将数据传递给上下文
ctx = context.WithValue(ctx, UserIDKey, 12345)
go doTask(ctx)
time.Sleep(1 * time.Second) // 等待goroutine执行
}
6. context
的取消与清理
每当使用 context.WithCancel
、context.WithTimeout
或 context.WithDeadline
时,记得使用 defer cancel()
来确保在不再需要该 context
时进行清理。这会帮助避免泄漏和过度消耗资源。
7. context
的嵌套与传递
context
是可以嵌套的。当你通过 WithCancel
、WithTimeout
等函数创建子 context
时,子 context
会继承父 context
的值和状态。如果父 context
被取消或超时,子 context
也会被取消。
示例:
func main() {
ctx := context.Background()
// 创建一个具有 3 秒超时的子 context
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
// 创建另一个子 context,并在其中传递数据
ctx = context.WithValue(ctx, "userID", 42)
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务被取消:", ctx.Err())
userID := ctx.Value("userID")
fmt.Println("用户 ID:", userID)
}
}(ctx)
time.Sleep(4 * time.Second)
}
在上面的代码中,子context
会继承父context
的取消机制,并且传递了用户 ID 的信息。