wavelet init

This commit is contained in:
ryan
2026-06-18 15:24:48 +08:00
parent d6a7011885
commit 99738bbc17
714 changed files with 139987 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
---
name: go-context
description: 在 Go 中使用 context.Context 时使用 — 包括函数签名中的位置、传播取消和截止时间、以及在 context 中存储值与使用参数的对比。也适用于取消长时间运行的操作、设置超时或传递请求作用域数据,即使未直接提及 context.Context。不涵盖 goroutine 生命周期或 sync 原语(参见 go-concurrency)。
license: Apache-2.0
compatibility: 需要 Go 1.7+(context 在 Go 1.7 中移入标准库)
metadata:
sources: "Go Wiki CodeReviewComments"
---
# Go Context 用法
## Context 作为第一个参数
使用 Context 的函数应将其作为**第一个参数**:
```go
func F(ctx context.Context, /* 其他参数 */) error
func ProcessRequest(ctx context.Context, req *Request) (*Response, error)
```
这是 Go 中的一个强约定,使 context 的传递在代码库中可见且一致。
---
## 不要在结构体中存储 Context
不要在结构体类型中添加 Context 成员。相反,将 `ctx` 作为参数传递给每个需要它的方法:
```go
// 不好:Context 存储在结构体中
type Worker struct {
ctx context.Context // 不要这样做
}
// 好:Context 传递给方法
type Worker struct{ /* ... */ }
func (w *Worker) Process(ctx context.Context) error {
// Context 显式传递 — 生命周期清晰
}
```
**例外**:签名必须匹配标准库或第三方库中接口的方法可能需要变通处理。
---
## 不要创建自定义 Context 类型
不要创建自定义的 Context 类型或在函数签名中使用 `context.Context` 以外的接口:
```go
// 不好:自定义 context 类型
type MyContext interface {
context.Context
GetUserID() string
}
// 好:使用标准 context.Context 并提取值
func Process(ctx context.Context) error {
userID := GetUserID(ctx)
}
```
---
## 应用数据放在哪里
按以下优先级顺序考虑:
1. **函数参数** — 最明确且类型安全
2. **接收者** — 适用于属于该类型的数据
3. **全局变量** — 适用于真正的全局配置(谨慎使用)
4. **Context 值** — 仅用于请求作用域数据
Context 值适用于:
- 请求 ID 和追踪 ID
- 随请求流动的认证/授权信息
- 截止时间和取消信号
Context 值**不适用**于:
- 可选的函数参数
- 可以显式传递的数据
- 不随请求变化的配置
---
## 常见模式
> 在派生 context(WithTimeout、WithCancel、WithDeadline)、在循环或 HTTP 处理器中检查取消、使用带类型键的 context 值、或需要快速参考表时,阅读 [references/PATTERNS.md](references/PATTERNS.md)。
### 派生 Context
创建派生 context 后,始终立即 `defer cancel()`:
```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
```
### 检查取消
```go
select {
case <-ctx.Done():
return ctx.Err()
default:
// 执行工作
}
```
### Context 不可变性
Context 是不可变的 — 将同一个 `ctx` 传递给共享相同截止时间和取消信号的多个并发调用是安全的。
---
## 相关技能
- **Goroutine 协调**:在使用 context 进行 goroutine 取消、基于 select 的超时或 errgroup 时,参见 [go-concurrency](../go-concurrency/SKILL.md)
- **错误处理**:在决定如何包装或返回 `ctx.Err()` 取消错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **接口设计**:在设计接受 context 并结合接口的 API 时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **请求作用域日志**:在将 logger 注入 context 或将请求 ID 添加到结构化日志输出时,参见 [go-logging](../go-logging/SKILL.md)
@@ -0,0 +1,227 @@
# Context 模式
派生、检查和传播 `context.Context` 的常见模式。
---
## Context 不可变性
Context 是不可变的。将同一个 `ctx` 传递给共享相同截止时间、取消信号、凭据和父级追踪的多个调用是安全的:
```go
// 安全:同一个 context 传递给顺序调用
func ProcessBatch(ctx context.Context, items []Item) error {
for _, item := range items {
if err := process(ctx, item); err != nil {
return err
}
}
return nil
}
// 安全:同一个 context 传递给并发调用
func ProcessConcurrently(ctx context.Context, a, b *Data) error {
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return processA(ctx, a) })
g.Go(func() error { return processB(ctx, b) })
return g.Wait()
}
```
---
## 何时使用 context.Background()
仅在**从不特定于请求**的函数中使用 `context.Background()`:
```go
func main() {
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func startBackgroundWorker() {
ctx := context.Background()
go worker(ctx)
}
```
**默认传递 Context**,即使你认为不需要。只有在有充分理由说明传递 context 是错误做法时,才直接使用 `context.Background()`:
```go
func LoadConfig(ctx context.Context) (*Config, error) {
// 即使现在不使用 ctx,接受它可以在未来添加功能时
// 不需要修改 API
}
```
---
## 派生 Context
```go
// 添加超时 — 持续时间结束后触发取消
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 添加取消 — 调用者控制何时取消
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// 添加截止时间 — 在指定的墙钟时间触发取消
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Hour))
defer cancel()
// 添加值(谨慎使用 — 仅用于请求作用域数据)
ctx = context.WithValue(ctx, requestIDKey, reqID)
```
创建派生 context 后,**始终立即 `defer cancel()`**。这确保即使函数提前返回,资源也会被释放。
### 嵌套派生
派生的 context 形成树状结构。取消父级会取消所有子级:
```go
func handleRequest(ctx context.Context) error {
// 整个请求的父级超时
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// 数据库调用的更短超时
dbCtx, dbCancel := context.WithTimeout(ctx, 5*time.Second)
defer dbCancel()
data, err := queryDB(dbCtx)
if err != nil {
return err
}
// 父级 context 的剩余时间适用于此处
return sendResponse(ctx, data)
}
```
---
## 检查取消
### 在长时间运行的循环中
```go
func LongRunningOperation(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 执行工作
}
}
}
```
### 在高开销操作之前
在开始无法中断的工作之前检查取消:
```go
func ProcessItems(ctx context.Context, items []Item) error {
for _, item := range items {
if ctx.Err() != nil {
return ctx.Err()
}
if err := expensiveProcess(item); err != nil {
return err
}
}
return nil
}
```
### 区分取消原因
```go
if err := ctx.Err(); err != nil {
switch {
case errors.Is(err, context.Canceled):
// 调用者显式取消(例如客户端断开连接)
case errors.Is(err, context.DeadlineExceeded):
// 超时或截止时间已过
}
}
```
---
## 在 HTTP 处理器中遵守取消
```go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := slowOperation(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// 客户端已断开连接 — 无需写入
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
```
`r.Context()` 在以下情况被取消:
- 客户端关闭连接
- `http.Server` 的 `ReadTimeout` 或 `WriteTimeout` 触发
- `ServeHTTP` 方法返回
---
## Context 值的最佳实践
### 使用未导出的键类型
```go
type contextKey struct{}
var userIDKey contextKey
func WithUserID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(userIDKey).(string)
return id, ok
}
```
使用未导出的结构体类型作为键可以防止与其他包的键发生冲突 — 即使它们使用相同的 string 或 int 值。
### 提供访问器函数
始终将 `context.WithValue` 和 `ctx.Value` 包装在类型化的辅助函数中(如上所示),而不是暴露键。这提供了类型安全性和一个可以修改实现的单一位置。
---
## 快速参考
| 模式 | 指导 |
|------|------|
| 参数位置 | 始终第一个:`func F(ctx context.Context, ...)` |
| 结构体存储 | 不要存储在结构体中;传递给方法 |
| 自定义类型 | 不要创建;使用 `context.Context` 接口 |
| 应用数据 | 优先选择 参数 > 接收者 > 全局变量 > context 值 |
| 请求作用域数据 | 适用于 context 值 |
| 共享 context | 安全 — context 是不可变的 |
| `context.Background()` | 仅用于非请求特定的代码 |
| 默认行为 | 即使认为不需要也要传递 context |
| `defer cancel()` | 在 `WithTimeout`/`WithCancel`/`WithDeadline` 之后始终立即 defer |
| 值键 | 使用未导出的结构体类型,提供访问器函数 |
| 取消检查 | 在高开销操作前使用 `ctx.Err()`;在循环中使用 `select` 监听 `ctx.Done()` |