5.7 KiB
5.7 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-07 14:55 |
Context
概述
Context 是 Go 中跨 goroutine 传递取消信号、截止时间、请求作用域值的标准机制。它是构建可取消、可超时、可追踪的并发程序的核心工具。
正文
Context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool) // 截止时间
Done() <-chan struct{} // 取消信号通道
Err() error // 取消原因
Value(key interface{}) interface{} // 键值对存储
}
[!question] 💭 思考 当一个 HTTP 请求被客户端断开连接时,如何通知所有正在处理的 goroutine 立即停止?
四个方法各司其职:
Deadline:返回截止时间(如果设置了)Done:返回只读 channel,关闭时表示应停止工作Err:返回取消原因(context.Canceled或context.DeadlineExceeded)Value:获取请求级别的上下文信息(如 traceID、用户信息)
创建根 Context
ctx := context.Background() // 空 context,不可取消,作为所有 context 的祖先
ctx := context.TODO() // 不确定该用哪个 context 时的占位
[!note] 📝 Background vs TODO
Background():已知没有父 context 时使用(如 main 函数入口)TODO():暂时不知道用什么 context,后续会替换——相当于"先写代码,稍后完善"
Context 派生树
[!info] ℹ️ Context 的传播链 Context 通过 With 系列函数层层派生,形成一棵树。取消一个父 context,所有子 context 都会收到取消信号。
flowchart TD
BG["background"] --> C1["WithCancel"]
C1 --> C2["WithTimeout"]
C1 --> C3["WithValue"]
C2 --> C4["WithValue"]
style BG fill:#e8f5e9
style C1 fill:#fff3e0
style C2 fill:#e3f2fd
style C3 fill:#fce4ec
style C4 fill:#f3e5f5
| 派生函数 | 功能 | 典型场景 |
|---|---|---|
WithCancel |
手动取消 | 业务逻辑主动终止 |
WithTimeout |
超时自动取消 | RPC 调用、HTTP 请求 |
WithDeadline |
指定时刻取消 | 定时任务截止 |
WithValue |
传递请求级数据 | traceID、用户认证信息 |
并发控制模式
WithCancel — 手动取消
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "observer1")
go watch(ctx, "observer2")
time.Sleep(5 * time.Second)
cancel() // 通知所有监听者退出
time.Sleep(time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: %v\n", name, ctx.Err())
return
default:
fmt.Printf("%s working...\n", name)
time.Sleep(time.Second)
}
}
}
[!tip] 💡 必记习惯:cancel 函数要调用
ctx, cancel := context.WithTimeout(parent, 5*time.Second) defer cancel() // 延迟释放资源,即使提前返回也不会泄漏
WithTimeout / WithDeadline
// 超时 5 秒
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 截止到明天中午 12 点
ctx, cancel := context.WithDeadline(context.Background(), tomorrowNoon)
defer cancel()
[!note] 📝 Timeout vs Deadline
WithTimeout(d)= 从现在开始经过 d 时间后超时WithDeadline(t)= 在绝对时间点 t 超时- 大多数情况下使用
WithTimeout更直观
WithValue — 传递请求级数据
type contextKey string
const userIDKey contextKey = "userID"
func handler(w http.ResponseWriter, r *http.Request) {
// 从请求头获取用户 ID,放入 context
ctx := context.WithValue(r.Context(), userIDKey, getUserID(r))
// 传递给下游 goroutine
processRequest(ctx)
}
func processRequest(ctx context.Context) {
if id := ctx.Value(userIDKey); id != nil {
log.Printf("processing for user %v", id)
}
}
[!warning] ⚠️ Value 的使用禁忌
- 不要用 Context 传递可选参数——那是函数参数的职责
- key 不要使用 string/int 类型——容易冲突,自定义 type 作为 key
- Value 是不可变的——子 context 只能读取不能修改父的值
- 不用于传递业务核心数据——这些数据应该作为函数参数显式传递
完整实战:带超时的 RPC 链路
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 1. 基于请求创建 context(携带超时和 traceID)
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
ctx = context.WithValue(ctx, traceIDKey, generateTraceID())
// 2. 并行调用多个下游服务
resultCh := make(chan Result, 2)
go func() {
resultCh <- callServiceA(ctx)
}()
go func() {
resultCh <- callServiceB(ctx)
}()
// 3. 等待结果或超时
select {
case r1 := <-resultCh:
handleResult(r1)
case r2 := <-resultCh:
handleResult(r2)
case <-ctx.Done():
http.Error(w, "request timeout", http.StatusGatewayTimeout)
}
}
[!warning] ⚠️ Context 传播的最佳实践
- Context 必须作为函数的第一个参数,命名为
ctx- 永远用传入的 context 创建新的 context,不要自己新建 background
- 所有网络调用、DB 查询都应接受 context 并检查取消信号
- 不要在 goroutine 中忽略 context.Done()——否则会导致 goroutine leak