在 Go 的并发编程中,context(上下文)包是绝对的核心。无论是处理 HTTP 请求、RPC 调用,还是数据库查询,Context 都扮演着控制 Goroutine 生命周期的角色。
简而言之,Context 主要解决三个问题: 1. 取消信号:通知子 Goroutine 停止工作,释放资源。 2. 超时控制:规定任务必须在多长时间内完成,否则强制取消。 3. 数据传递:在调用链中传递请求范围内的元数据(如 UserID, TraceID)。
1. 为什么需要 Context?
假设你启动了一个 Goroutine 去查询数据库,如果用户突然关闭了浏览器,或者请求处理太慢超时了,你希望这個后台查询任务能立即停止,而不是继续浪费数据库资源。这就是 Context 的用武之地。
Go 的设计原则:谁启动了 Goroutine,谁就有责任(通过 Context)管理它的退出。
2. 超时控制 (WithTimeout)
这是 Context 最常用的场景。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. 创建一个带超时的 Context
// 规定任务必须在 2 秒内完成,否则 ctx 会收到取消信号
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
// 关键:函数结束时必须调用 cancel,防止内存泄漏
defer cancel()
fmt.Println("Start working...")
// 2. 将 ctx 传递给耗时任务
doSlowWork(ctx)
}
func doSlowWork(ctx context.Context) {
// 模拟一个需要 3 秒才能完成的任务
// Select 会等待 done 信号或者 work 完成,以此来实现超时抢占
select {
case <-time.After(3 * time.Second): // 模拟业务逻辑
fmt.Println("Work done successfully!")
case <-ctx.Done(): // 监听 Context 的取消信号
// 如果超时了,ctx.Err() 会返回 DeadlineExceeded
fmt.Println("Work cancelled:", ctx.Err())
}
}运行结果:
由于任务耗时 3秒,但 Context 限制 2秒,所以输出:
Work cancelled: context deadline exceeded
如果您将任务时间改为 1秒,输出则是:
Work done successfully!
3. 级联取消 (WithCancel)
Context 是树状结构的。当父 Context 被取消时,所有基于它派生的子 Context 也会自动被取消。
func main() {
// 创建一个可手动取消的 Context
ctx, cancel := context.WithCancel(context.Background())
// 启动子任务
go speak(ctx)
time.Sleep(1 * time.Second)
fmt.Println("Boss: Shut up now!")
// 发送取消信号
cancel()
// 等待一会看效果
time.Sleep(1 * time.Second)
}
func speak(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker: OK, I stop.")
return // 必须 return 退出 Goroutine
default:
fmt.Println("Worker: Blah blah...")
time.Sleep(300 * time.Millisecond)
}
}
}4. 传递数据 (WithValue)
可以使用 Context 在函数调用链中传递少量元数据。 注意:不要用它传递业务参数(如函数入参),只用来传那些“不仅限于当前函数”的信息,比如 TraceID、认证 Token。
func main() {
// 存入数据
ctx := context.WithValue(context.Background(), "userID", 10086)
processRequest(ctx)
}
func processRequest(ctx context.Context) {
// 取出数据
// Value 返回的是 interface{},需要类型断言
if uid, ok := ctx.Value("userID").(int); ok {
fmt.Printf("Processing request for User ID: %d\n", uid)
}
}5. 最佳实践
首选参数:
Context通常作为函数的第一个参数,命名为ctx。不要传 nil:如果你还没想好用啥 Context,请用
context.TODO(),不要传nil。不可变性:Context 是并发安全的,一旦创建不可修改(只能通过
WithXxx派生新 Context)。即使超时也要 cancel:使用
WithTimeout后,依然要defer cancel()。虽然超时会自动取消,但显式调用 cancel 是一种更安全的资源清理习惯。
小结
Context 是 Go 并发编程的神经系统。掌握了 WithTimeout 和 WithCancel,你就能编写出健壮的、不会轻易发生 Goroutine 泄漏的现代 Go 程序。
练习题
编写一个 HTTP 客户端,请求
http://google.com,使用 Context 限制请求超时时间为 100 毫秒(这通常会失败并触发超时)。编写一个 worker 池,主函数通过调用
cancel()一次性通知所有 worker 停止工作。