Go中的context 包使用详解
context
包在 Go 中非常重要,特别是在处理并发和超时的场景下,它能让你在多个 goroutine 之间传递取消信号、超时控制或其他控制信息。context
是 Go 并发模型中的一个重要工具,尤其适用于 HTTP 请求、数据库操作、分布式系统等场景。
1. 基本概念
context
包的主要功能是提供一个 Context
类型,允许在多个 goroutine 之间共享请求范围的信息,并在需要时提供取消信号。
Context
是不可变的,一旦创建就不能修改。每次操作都返回一个新的 Context
对象。
2. 常见用法
- 传递取消信号:在多个 goroutine 之间传递取消信号。
- 设置超时:为操作设置超时限制。
- 传递请求范围的值:可以在上下文中传递请求的元数据(如用户 ID、请求 ID)。
3. context
包的基本结构
context
包提供了几种不同类型的 Context
,最常用的包括:
context.Background()
:根上下文,通常用于程序的顶层。context.TODO()
:用于暂时没有明确上下文的地方,表示一个不确定的上下文。context.WithCancel()
:创建一个可取消的上下文。context.WithTimeout()
:创建一个带有超时的上下文。context.WithDeadline()
:创建一个带有具体截止时间的上下文。context.WithValue()
:创建一个可以存储值的上下文。
4. context.Background()
和 context.TODO()
context.Background()
通常用于程序的顶层调用,表示一个没有附加元数据的空上下文。它通常作为其他上下文的根基。context.TODO()
用于你不确定当前使用哪种上下文的地方,表示暂时还没有决定。
ctx := context.Background() // 用作根上下文
// 或者
ctx := context.TODO() // 用作还未确定上下文的地方
5. context.WithCancel()
context.WithCancel()
创建一个可以手动取消的上下文。当你取消这个上下文时,所有基于该上下文的 goroutine 都会收到取消信号。
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Work cancelled:", ctx.Err())
return
default:
// 模拟一些工作
fmt.Println("Working...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 创建一个可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
// 启动一个 goroutine 执行任务
go doWork(ctx)
// 模拟执行一段时间后取消
time.Sleep(3 * time.Second)
cancel() // 取消上下文,通知 goroutine 停止工作
// 等待 goroutine 完成
time.Sleep(1 * time.Second)
}
输出:
Working...
Working...
Working...
Work cancelled: context canceled
在这个例子中,cancel()
触发了 ctx.Done()
的信号,通知 doWork
函数退出。
6. context.WithTimeout()
和 context.WithDeadline()
context.WithTimeout()
创建一个具有超时限制的上下文,超时后自动取消。context.WithDeadline()
类似,但你指定的是一个具体的截止时间。
示例:使用 WithTimeout
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
select {
case <-time.After(2 * time.Second): // 模拟耗时操作
fmt.Println("Work completed")
case <-ctx.Done():
fmt.Println("Work cancelled:", ctx.Err())
}
}
func main() {
// 创建一个带有 1 秒超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 启动 goroutine 执行任务
go doWork(ctx)
// 等待 2 秒,超过了超时时间
time.Sleep(2 * time.Second)
}
输出:
Work cancelled: context deadline exceeded
在这个例子中,由于上下文在 1 秒后超时,doWork
会在超时后取消任务。
示例:使用 WithDeadline
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
select {
case <-time.After(2 * time.Second): // 模拟耗时操作
fmt.Println("Work completed")
case <-ctx.Done():
fmt.Println("Work cancelled:", ctx.Err())
}
}
func main() {
// 创建一个截止时间为当前时间加 1 秒的上下文
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 启动 goroutine 执行任务
go doWork(ctx)
// 等待 2 秒,超出了截止时间
time.Sleep(2 * time.Second)
}
输出:
Work cancelled: context deadline exceeded
7. context.WithValue()
context.WithValue()
允许你在上下文中存储键值对,以便在不同的 goroutine 之间共享数据。通常用于请求 ID 或其他元数据的传递。
package main
import (
"context"
"fmt"
)
type key string
func main() {
// 创建一个上下文并在其中存储一个值
ctx := context.WithValue(context.Background(), key("userID"), 42)
// 从上下文中获取值
userID := ctx.Value(key("userID"))
fmt.Println("UserID:", userID)
}
输出:
UserID: 42
8. context
在并发中的典型应用场景
- HTTP 请求处理:在 web 应用中,你通常会为每个请求创建一个
context
,以便在请求生命周期内传递取消信号、超时、身份验证信息等。 - 数据库操作:当进行数据库查询时,可以使用
context
来设置超时、取消信号等,确保操作不会因超时而挂起。 - 分布式任务:在微服务架构中,
context
可以在服务之间传递请求信息,并确保某个操作超时或取消时其他相关操作也会停止。
9. context
的使用注意事项
- 不要将
context
存储在结构体或 global 变量中,因为context
是短生命周期的,应该只在请求的生命周期内传递。 - 不要将
context
用作存储实际的数据,它主要用于传递控制信号(如取消信号、超时等)。 - 避免重复创建:在每次使用
context.WithXXX
时,应该返回一个新的Context
,并通过defer cancel()
确保在操作完成后释放资源。
总结
context
是 Go 中非常重要的并发工具,适用于传递取消信号、超时控制和请求范围的数据。- 主要的创建函数包括
WithCancel
、WithTimeout
、WithDeadline
和WithValue
,它们允许在不同的场景下创建上下文。 context
的传递机制让我们能够在多个 goroutine 之间共享控制信息,如取消信号、超时或元数据,避免了手动的锁机制,使并发编程更加简洁和高效。