195 lines
5.7 KiB
Markdown
195 lines
5.7 KiB
Markdown
---
|
||
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原理]]
|