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

195 lines
5.7 KiB
Markdown
Raw Normal View History

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