关于 go-context
go-context 是面向 Go 开发者的项目技能,指导如何管理取消、截止时间和请求级值,并通过按需资料说明 API、HTTP 处理器及测试中的安全传递方式。
主要功能
- 01Context 参数约定
- 02取消与超时处理
- 03请求级值的使用指导
- 04HTTP 与测试模式
- 05取消原因与分离任务
适用场景
- 01审查 Go API 中的 Context 用法
- 02避免 goroutine 和计时器泄漏
- 03测试取消与超时行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-context安装后在 Claude Code 中输入/go-context确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/cause-and-detach.mdhttp-and-testing.mdtimeout-budgets.mdvalues.md详细介绍
Go Context
context.Context 用于在 API 边界之间控制取消、截止时间和请求范围内的值。误用它会导致 goroutine 泄漏、工作任务失去管理,以及难以察觉的生产环境 Bug。
社区默认规则。若项目技能或 Agent 配置规则明确取代
go-context,则以该规则为准。
按需加载的详细参考资料:
references/timeout-budgets.md— 如何分配父级剩余预算,以及如何读取ctx.Deadline()。references/values.md— context 键类型、访问器和冲突陷阱。references/http-and-testing.md— 处理器和中间件中的请求 context,以及取消测试。references/cause-and-detach.md—WithCancelCause、WithTimeoutCause、WithoutCancel、AfterFunc。
仅当下文内容不足时,才阅读参考文件。
1. 核心规则
Context 始终是第一个参数:
// ✅ Good — context is first
func GetUser(ctx context.Context, id string) (*User, error)
func (s *Service) Process(ctx context.Context, req Request) error
// ❌ Bad — context buried in the middle or end
func GetUser(id string, ctx context.Context) (*User, error)
func Process(req Request, ctx context.Context) error
绝不要将 context 存储在结构体中:
// ❌ Bad — context stored in struct
type Server struct {
ctx context.Context // NEVER do this
cancel context.CancelFunc
}
// ✅ Good — pass context through method parameters
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}
Context 表示单次操作的生命周期,而非对象的生命周期。
绝不要传递 nil context:
// ❌ Bad
doSomething(nil, data)
// ✅ Good — use context.TODO() if unsure which context to use
doSomething(context.TODO(), data)
// ✅ Good — use context.Background() for top-level/main
doSomething(context.Background(), data)
2. 取消
始终延迟调用 cancel:
// ✅ Good — cancel called even if operation succeeds
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
result, err := longOperation(ctx)
未调用 cancel 会导致资源(计时器、goroutine)泄漏,直到父级 context 被取消。
手动取消时使用 WithCancel:
func (s *Supervisor) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return s.runWorkerA(ctx) })
g.Go(func() error { return s.runWorkerB(ctx) })
// If any worker returns an error, errgroup cancels ctx,
// which signals all other workers to stop.
return g.Wait()
}
在循环中检查 context 是否已取消:
// ✅ Good — respects cancellation
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
if err := ctx.Err(); err != nil {
return fmt.Errorf("processing cancelled: %w", err)
}
if err := process(ctx, item); err != nil {
return fmt.Errorf("process item %s: %w", item.ID, err)
}
}
return nil
}
// ❌ Bad — runs to completion even if cancelled
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
process(ctx, item) // ignores ctx cancellation between items
}
return nil
}
3. 超时和截止时间
基于时长的限制使用 WithTimeout:
func (c *Client) FetchUser(ctx context.Context, id string) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/users/"+id, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch user %s: %w", id, err)
}
defer resp.Body.Close()
// ...
}
基于绝对时间的限制使用 WithDeadline:
// Use when coordinating with external deadlines (SLAs, cron windows)
deadline := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
超时预算
子级超时会消耗父级预算的一部分,绝不会延长预算。在一个 30s 的请求处理器中,5s 的 DB 调用和 10s 的 API 调用都属于预算的一部分。若父级只剩 5s,而子级设置为 60s,那么它会在父级截止时间到达时悄然触发;凌晨 3 点时,这看起来就像个 Bug。
开始无法中断的工作之前,使用 ctx.Deadline() 读取剩余预算;如果预算太短,就快速失败。
示例见 references/timeout-budgets.md。
4. Context 值
仅存储请求范围内的元数据:请求 ID、跟踪 ID 或 span ID、已认证用户、请求范围内的日志记录器。绝不要存储数据库连接、配置,或任何可由函数显式接收为参数的内容。
键必须使用未导出的类型,绝不能使用字符串——字符串键可能被进程中的任何其他包覆盖。导出访问器函数,让调用方无需自行对 ctx.Value 的结果进行类型断言。
示例见 references/values.md。
5. HTTP 处理器和测试中的 Context
处理器接收 r.Context() 并将其传递给下游。请求已取消意味着客户端已断开连接:直接返回,不要写入响应。中间件使用 r.WithContext(ctx) 附加值。
测试应从 t.Context()(Go 1.24+)派生 context,而非使用 context.Background();用 context.WithTimeout 包裹待测调用,这样发生挂起时测试会失败,而不会阻塞整个测试套件;并断言 errors.Is(err, context.Canceled),以证明取消确实得到处理。
示例见 references/http-and-testing.md。
6. 原因、脱离请求的工作和 AfterFunc
| 需求 | API(Go 版本) |
|---|---|
| 记录工作取消的原因 | WithCancelCause + context.Cause(1.20) |
| 指明触发的是哪种超时 | WithTimeoutCause、WithDeadlineCause(1.21) |
| 请求返回后继续完成工作,同时保留其值 | WithoutCancel + 自行设置超时(1.21) |
| 解除不接受 context 的调用的阻塞 | AfterFunc(1.21) |
脱离请求的工作始终要设置自己的超时。context.Background() 同样可以让工作在请求结束后继续,但它会丢弃跟踪 ID。示例见:references/cause-and-detach.md。
7. context.Background() 与 context.TODO()
| 函数 | 适用场景 |
|---|---|
context.Background() | 顶层入口:main()、init()。有意使用的根 context。测试使用 t.Context()(Go 1.24+)。 |
context.TODO() | 尚不确定该使用哪个 context 时的占位符。表示“此处需要修复”。 |
在生产代码中,context.TODO() 是一种代码异味——发布前应将其替换。
验证清单
- 在所有接受
context.Context的函数中,它都是第一个参数 - 不在结构体字段中存储 context
- 在调用
WithCancel、WithTimeout、WithDeadline后立即调用defer cancel() - 长循环在每次迭代之间检查
ctx.Err() - 子超时不超过父超时的剩余时间
- Context 值使用非导出键类型,并提供访问器函数
- Context 值中只存储请求范围的元数据(而非配置、连接)
- HTTP 处理程序使用
r.Context()并向下游传递 - 不传递
nilcontext——使用context.TODO()或context.Background() - 测试使用
context.WithTimeout来防止挂起 - 生命周期超出请求的工作使用
WithoutCancel并设置自己的超时,而不是使用context.Background()