mcpField
G
go-context

go-context

github.com/eduardo-sl开发86180 次安装3 周前更新
代码仓库

关于 go-context

go-context 是面向 Go 开发者的项目技能,指导如何管理取消、截止时间和请求级值,并通过按需资料说明 API、HTTP 处理器及测试中的安全传递方式。

主要功能

  1. 01Context 参数约定
  2. 02取消与超时处理
  3. 03请求级值的使用指导
  4. 04HTTP 与测试模式
  5. 05取消原因与分离任务

适用场景

  1. 01审查 Go API 中的 Context 用法
  2. 02避免 goroutine 和计时器泄漏
  3. 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.md
http-and-testing.md
timeout-budgets.md
values.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() 是一种代码异味——发布前应将其替换。

验证清单

  1. 在所有接受 context.Context 的函数中,它都是第一个参数
  2. 不在结构体字段中存储 context
  3. 在调用 WithCancel、WithTimeout、WithDeadline 后立即调用 defer cancel()
  4. 长循环在每次迭代之间检查 ctx.Err()
  5. 子超时不超过父超时的剩余时间
  6. Context 值使用非导出键类型,并提供访问器函数
  7. Context 值中只存储请求范围的元数据(而非配置、连接)
  8. HTTP 处理程序使用 r.Context() 并向下游传递
  9. 不传递 nil context——使用 context.TODO() 或 context.Background()
  10. 测试使用 context.WithTimeout 来防止挂起
  11. 生命周期超出请求的工作使用 WithoutCancel 并设置自己的超时,而不是使用 context.Background()

评论

支持 Markdown