在 Go 语言开发中,
context是一个非常重要的标准库,几乎所有现代 Go 项目都会使用它。无论是 Web 服务、微服务架构、数据库请求还是 goroutine 控制,都离不开context。
本文将系统讲解:
- context 是什么
- 为什么需要 context
- 常见 API 使用
- 实际开发场景
- 最佳实践与常见坑
一、context 是什么
context 是 Go 标准库中的一个包,用于在多个 goroutine 之间 传递控制信号、取消任务以及共享请求数据。
简单理解:
context 是 控制 goroutine 生命周期的工具。
主要功能包括:
- 取消 goroutine
- 控制请求超时
- 在函数间传递请求信息
- 控制并发任务生命周期
在 Go 官方设计中:
context 用于 跨 API 边界传递请求范围的数据和取消信号。
二、为什么需要 context
在 Go 的早期版本中,如果启动多个 goroutine,通常会遇到这样的问题:
任务无法优雅停止。
例如:
go
go doTask()
如果:
- HTTP 请求已经结束
- 用户取消操作
- 程序超时
后台 goroutine 仍然会继续运行。
这会导致:
- 内存泄漏
- goroutine 泄漏
- 资源浪费
context 的出现就是为了解决这个问题。
三、context 的核心设计
context 的核心思想是:
父任务可以控制所有子任务。
结构类似一棵树:
css
request
├── goroutine A
│ ├── task A1
│ └── task A2
└── goroutine B
└── task B1
当父 context 被取消时:
所有子任务都会收到取消信号。
四、context 的四种创建方式
Go 中主要有四种 context 创建方式。
1 Background
go
ctx := context.Background()
特点:
- 根 context
- 不会被取消
- 没有超时
通常用于:
- main 函数
- 程序入口
- 初始化代码
2 TODO
go
ctx := context.TODO()
含义:
开发者还没有确定使用哪个 context。
主要用于:
- 临时代码
- 占位 context
生产代码建议使用 Background。
3 WithCancel
go
ctx, cancel := context.WithCancel(parent)
可以手动取消 context。
示例:
go
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-ctx.Done()
fmt.Println("任务被取消")
}()
cancel()
调用 cancel 后:
所有监听 ctx.Done() 的 goroutine 都会停止。
4 WithTimeout
go
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
设置超时时间。
示例:
go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务超时")
}
5 WithDeadline
go
ctx, cancel := context.WithDeadline(parent, deadline)
指定具体时间点。
示例:
go
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
五、context 的三个核心方法
context 接口非常简单:
go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
下面分别介绍。
1 Done()
返回一个 channel。
当 context 被取消或超时时:
channel 会关闭。
常见写法:
go
select {
case <-ctx.Done():
return
}
2 Err()
获取 context 结束原因。
可能返回:
context.Canceled
context.DeadlineExceeded
示例:
go
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("超时")
}
3 Value()
用于在函数间传递数据。
示例:
go
ctx := context.WithValue(context.Background(), "userID", 123)
value := ctx.Value("userID")
但官方建议:
不要滥用 Value。
主要用于:
- request ID
- 用户信息
- trace 信息
六、context 实际使用示例
下面是一个典型 goroutine 控制例子。
go
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 退出")
return
default:
fmt.Println("worker 运行中")
time.Sleep(time.Second)
}
}
}
启动任务:
go
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(3 * time.Second)
cancel()
输出:
worker 运行中
worker 运行中
worker 运行中
worker 退出
七、HTTP 服务中的 context
Go HTTP 框架大量使用 context。
例如:
go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(5 * time.Second):
fmt.Println("处理完成")
case <-ctx.Done():
fmt.Println("客户端取消请求")
}
}
当客户端断开连接:
context 会自动取消。
八、数据库查询中的 context
数据库操作也支持 context。
例如:
go
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
如果:
- 查询超时
- 请求取消
数据库操作会自动终止。
九、context 使用最佳实践
1 context 作为第一个参数
官方推荐:
go
func DoSomething(ctx context.Context)
2 不要存储 context
错误做法:
go
type Service struct {
ctx context.Context
}
context 应该作为函数参数传递。
3 不要传 nil
错误:
go
func Test(ctx context.Context)
Test(nil)
应该使用:
scss
context.Background()
4 cancel 必须调用
如果使用:
WithCancel
WithTimeout
WithDeadline
必须调用:
scss
cancel()
否则可能造成资源泄漏。
十、常见错误
goroutine 泄漏
没有监听 ctx.Done():
go
go func() {
for {
work()
}
}()
应该写成:
go
select {
case <-ctx.Done():
return
}
滥用 WithValue
context 不是数据存储工具。
只适合传递:
- requestID
- traceID
- auth token
不适合传递业务数据。
十一、context 在微服务中的作用
在微服务架构中,context 还承担:
- 请求链路追踪
- 分布式日志
- 超时控制
- 取消级联
例如:
sql
API Gateway
↓
User Service
↓
Order Service
↓
Database
整个调用链共享一个 context。
如果请求超时:
所有服务都会停止处理。
十二、总结
context 是 Go 并发控制的核心组件。
主要解决的问题:
- goroutine 生命周期管理
- 请求取消控制
- 超时管理
- 请求数据传递
核心能力包括:
- cancel 控制
- timeout 控制
- goroutine 协调
在现代 Go 项目中,context 已经成为:
所有 API 的标准参数。
理解并正确使用 context,是写出高质量 Go 并发程序的重要基础。