Context 是 Go 中用来控制"一次任务生命周期"的工具。
可以用于取消任务、设置超时时间和在一次请求链路中传递一些上下文信息。
我们可以把 Context 想象成一个"遥控器"。
一个 HTTP 请求进来了:
text
用户请求
│
▼
HTTP Handler
│
├── 查询用户
│
├── 查询订单
│
└── 调用 AI 服务
如果用户突然关闭页面,我们希望后面的工作也别干了,以免浪费系统资源。
这时候,Context 就可以按下取消按钮,停止后续的任务。
Context 的三个功能
学习 Context 时,记住这三个词:
text
Context
│
├── Cancel 取消
│
├── Deadline 截止时间
│
└── Value 上下文数据
这三个词对应 Context 的类型。
Cancel
告诉任务:
别干了。
Deadline / Timeout
告诉任务:
最多干这么久。
Value
告诉下游:
这是这次请求携带的一些上下文信息。
案例一:手动取消任务
我们来看一个取消任务的例子:
go
package main
import (
"context"
"fmt"
"time"
)
func work(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("任务被取消")
return
default:
fmt.Println("正在工作...")
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go work(ctx)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(time.Second)
}
运行结果:
text
正在工作...
正在工作...
正在工作...
任务被取消
context.WithCancel() 创建一个带有取消功能的上下文。
go
ctx, cancel := context.WithCancel(context.Background())
ctx.Done() 会返回一个 Channel ,当 cancel() 被调用时,这个 Channel 就会被关闭,从而实现通知任务取消。
go
select {
case <-ctx.Done():
return
}
案例二:设置任务超时时间
我们来看使用 Context 设置任务超时时间的例子
go
func work(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("任务结束:", ctx.Err())
return
default:
fmt.Println("正在工作...")
time.Sleep(time.Second)
}
}
}
然后:
go
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
work(ctx)
}
大约 3 秒之后:
text
正在工作...
正在工作...
正在工作...
任务结束: context deadline exceeded
这里:
go
ctx.Err()
可以告诉我们 Context 为什么结束。
常见的结果包括:
go
context.Canceled
表示:
被主动取消了。
以及:
go
context.DeadlineExceeded
表示:
超时了。
案例三:传递上下文数据
我们来实现一个使用 Context 传递上下文数据的例子。
go
package main
import (
"context"
"fmt"
)
type ctxKey string // 自定义 key 类型,避免和别人的 key 撞车
const traceIDKey ctxKey = "traceID"
func withTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceIDKey, id)
}
func handle(ctx context.Context) {
if id, ok := ctx.Value(traceIDKey).(string); ok {
fmt.Println("traceID =", id)
}
}
func main() {
ctx := withTraceID(context.Background(), "req-42")
handle(ctx) // → traceID = req-42
}
运行结果:
text
traceID = req-42
注意 Context.Value 不是什么都能放,比如业务参数 、大型对象和普通函数参数就不适合放在 Context.Value 中。
Context 的 Value 适合传递 trace ID 、鉴权相关的请求上下文等请求域等数据。
key 一定要用自定义类型 ,不要用裸 string :
go
ctx = context.WithValue(ctx, "traceID", id) // ✗ 别的包可能也用 "traceID"
ctx = context.WithValue(ctx, traceIDKey, id) // ✓ 类型不导出,天然隔离
取出来必须做类型断言,并且不要存指针指向可变对象------Context 是并发安全的,但它存的值本身不会帮你加锁:
go
v := ctx.Value(traceIDKey)
id, ok := v.(string) // 一定要用带 ok 的形式,否则可能 panic
Context 可以一层一层传递
Context 厉害的地方,是它可以沿着调用链一路传递。
例如一个 HTTP 请求:
text
HTTP Request
│
▼
Handler
│
▼
Service
│
▼
Repository
│
▼
Database
我们可以让 Context 一路传下去:
go
func Handler(ctx context.Context) {
user := GetUser(ctx)
}
func GetUser(ctx context.Context) *User {
return queryDatabase(ctx)
}
func queryDatabase(ctx context.Context) *User {
// 使用 ctx 查询数据库
}
于是:
text
Handler
│
│ ctx
▼
Service
│
│ ctx
▼
Repository
│
│ ctx
▼
Database
如果最上面的请求取消:
text
HTTP 请求取消
│
▼
ctx
│
├── Handler
│
├── Service
│
└── Database
下游都可以感知到,类似于"遥控器"。
Context 的"签名约定"
Go 社区强制约定:
go
func DoSomething(ctx context.Context, a int, b string) error {
// ctx 永远是第一个参数,命名永远叫 ctx
}
这样一眼就能看出来:
这个函数支持 Context 控制。
Context 最常见的场景:HTTP 服务
在写 Go Web 服务的时候,我们一般都会使用 Context。
例如:
go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := GetUser(ctx, 1001)
if err != nil {
return
}
fmt.Fprintln(w, user)
}
这里:
go
r.Context()
就是当前 HTTP 请求对应的 Context。
你可以理解成:
text
浏览器
│
│ HTTP Request
▼
Go HTTP Server
│
▼
Request Context
│
├── Service
├── Database
└── Third-party API
因此,在 Web 服务中:
HTTP 请求的 Context 通常是整条调用链的"总指挥"。
Context + 数据库
数据库操作同样支持 Context。
例如:
go
rows, err := db.QueryContext(
ctx,
"SELECT * FROM users WHERE id = ?",
userID,
)
这里:
go
QueryContext(...)
意味着:
这个数据库查询属于这个 Context。
如果 Context 被取消,数据库驱动就有机会终止这个查询。
所以实际项目中经常看到:
go
func GetUser(ctx context.Context, id int) (*User, error) {
return db.QueryContext(
ctx,
"SELECT * FROM users WHERE id = ?",
id,
)
}
Context 是树结构
Context 还有一个非常重要的特点:
Context 是可以派生的。
例如:
go
root := context.Background()
ctx1, cancel1 := context.WithCancel(root)
ctx2, cancel2 := context.WithTimeout(
ctx1,
5*time.Second,
)
可以画成:
text
root
│
▼
ctx1
│
▼
ctx2
如果:
go
cancel1()
那么:
text
ctx1 ❌
│
└── ctx2 ❌
父 Context 被取消:
子 Context 也会被取消。
但是反过来:
go
cancel2()
只会影响:
text
ctx2 ❌
不会影响:
text
ctx1 ✅
所以 Context 的关系可以理解为:
父 Context 控制子 Context 的生命周期。
这也是为什么 Context 特别适合请求链路,例如:
text
HTTP Request Context
│
├── User Service
│ │
│ └── Database
│
├── Order Service
│ │
│ └── Database
│
└── AI Service
│
└── HTTP API
整个请求取消:
text
HTTP Request ❌
│
├── User Service ❌
├── Order Service ❌
└── AI Service ❌
这就形成了一个完整的生命周期控制体系。
context.Background() 是什么?
你经常会看到:
go
context.Background()
它是一个最基础的 Context。
可以把它理解成:
Context 世界的根节点。 ------一个永远不取消、没有截止时间、不携带任何值的空 ctx,只该出现在
main、初始化、测试这类调用链的起点。
例如:
go
ctx := context.Background()
然后:
go
ctx, cancel := context.WithCancel(ctx)
再:
go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
context.TODO() 是什么?
context.TODO() 的行为和 context.Background() 完全一样(永不取消、无超时、无值),差别只在语义 ------它是一句"我知道这里该有一个真正的 ctx,但暂时还没想好传哪个"的待办标记。
例如开发过程中:
go
func DoSomething(ctx context.Context) {
// ...
}
但调用方暂时没有 Context:
go
DoSomething(context.TODO())
可以先这样写。
不过在明确知道应该使用哪个 Context 时,优先使用真实的 Context。
又假设我们正在重构一个老项目,原来的代码没有 Context:
go
func GetUser(userID int) {
fmt.Println("查询用户:", userID)
}
现在我们希望让它支持 Context:
go
func GetUser(ctx context.Context, userID int) {
fmt.Println("查询用户:", userID)
}
但是项目中还有很多地方调用:
go
GetUser(1001)
暂时还没来得及把真正的 Context 传下来。
这时候可以临时改成:
go
GetUser(context.TODO(), 1001)
等以后把调用链改造完成,再替换成真正的 Context:
go
func Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
GetUser(ctx, 1001)
}
defer cancel() 为什么到处都是?
我们经常会看到:
go
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
可能有人会问:
"不是 5 秒之后自动取消吗?为什么还要
cancel()?"
因为:
go
defer cancel()
可以确保:
函数提前结束时,及时释放 Context 相关资源。
所以这是一个非常常见的 Go 写法:
go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
记住这个模式即可。
总结
如果用一句话解释 Context:
Context 是 Go 用来管理一次请求或任务生命周期的机制,可以让调用链中的函数共享取消信号、超时信息和少量请求范围的数据。
再简单一点:
Context 就是一个贯穿整个调用链的"任务遥控器"。
它主要负责三件事:
text
取消
超时
传递请求上下文
用一个接近生活的案例来类比:
假设你用手机在餐厅点了一份外卖。
text
手机
│
▼
餐厅
│
▼
厨师
│
▼
配菜
│
▼
外卖员
订单就是一次请求。
Context 就像订单上的一个"状态控制器"。
如果你在手机上取消订单:
text
订单取消
│
├── 厨师:别做了
├── 配菜:别准备了
└── 外卖员:别送了
这就是:
go
cancel()
如果订单超 30 分钟后自动取消订单,这就是:
go
context.WithTimeout(ctx, 30*time.Minute)
如果订单上还有:
text
订单号:A10086
用户:张三
TraceID:abc123
这些属于这次订单的上下文信息,可以类比:
go
context.WithValue(...)
所以:
Context 不是用来"做事情"的,而是用来管理事情怎么结束、什么时候结束,以及在这条调用链上携带哪些上下文信息。