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