Files
cs-note/hzh/GolangStar/Go语言进阶/Context.md
T

195 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, Context, 并发, 超时控制]
create time: 2026-06-07 14:55
---
# Context
## 概述
Context 是 Go 中跨 goroutine 传递取消信号、截止时间、请求作用域值的标准机制。它是构建可取消、可超时、可追踪的并发程序的核心工具。
## 正文
### Context 接口
```go
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
```go
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 都会收到取消信号。
```mermaid
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 — 手动取消
```go
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 函数要调用
> ```go
> ctx, cancel := context.WithTimeout(parent, 5*time.Second)
> defer cancel() // 延迟释放资源,即使提前返回也不会泄漏
> ```
#### WithTimeout / WithDeadline
```go
// 超时 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 — 传递请求级数据
```go
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 的使用禁忌
> 1. **不要用 Context 传递可选参数**——那是函数参数的职责
> 2. **key 不要使用 string/int 类型**——容易冲突,自定义 type 作为 key
> 3. **Value 是不可变的**——子 context 只能读取不能修改父的值
> 4. **不用于传递业务核心数据**——这些数据应该作为函数参数显式传递
### 完整实战:带超时的 RPC 链路
```go
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
## 关联笔记
- [[hzh/GolangStar/Go语言进阶/Select]]
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
- [[hzh/GolangStar/Go语言原理/context原理]]