agentsclimarketplace

Go context

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-context

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-context

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Guides Go context.Context discipline — pass it as the first parameter named ctx and never store it in a struct, derive children with WithCancel/WithTimeout/WithDeadline and ALWAYS defer cancel() (go vet's lostcancel flags a missing one), check ctx.Done()/ctx.Err() in loops and before expensive work, start from context.Background() at entry points and context.TODO() when you don't have one yet, attach reasons with WithCancelCause/Cause and decouple with WithoutCancel/AfterFunc (1.20/1.21), and carry only request-scoped values behind an unexported key type. Auto-invokes when writing or editing functions taking context.Context, WithCancel/WithTimeout/WithValue, ctx.Done()/Err(), or on "where should ctx go" / "is this context leaking" requests. The propagation-and-cancellation depth behind the policy root's "never start a goroutine you can't stop."

SKILL.md

16.0 KB, as published. Nobody here has run it

Go Context

"Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx." — context package docs

"At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests." — Go Concurrency Patterns: Context

A context.Context carries a deadline, a cancellation signal, and request-scoped values across API and process boundaries. It is plumbing for cancellation and propagation, not a bag for parameters. This skill owns where ctx goes, how you derive and cancel children, and what values may ride along; it routes goroutine lifetime to go-concurrency-goroutines, select mechanics to go-channels-select, and the Canceled/DeadlineExceeded sentinels to go-error-handling.


1. ctx Is the First Parameter, Never a Struct Field

The Context flows down the call tree as an explicit argument: "The Context should be the first parameter, typically named ctx" (context docs). The Google style guide is categorical: "Do not add a context member to a struct type. Instead, add a context parameter to each method on the type that needs to pass it along" and "When passed to a function or method, context.Context is always the first parameter" (Google Style Guide — Decisions).

// WRONG — ctx stored in a struct: obscures lifetime, freezes one scope for all calls
type Fetcher struct {
	ctx    context.Context // the request that built this Fetcher now binds every call
	client *http.Client
}
func (f *Fetcher) Get(url string) (*Response, error) { /* uses f.ctx */ }

// RIGHT — ctx is the first parameter of each call that needs it
type Fetcher struct {
	client *http.Client
}
func (f *Fetcher) Get(ctx context.Context, url string) (*Response, error) { /* uses ctx */ }

Storing ctx in a field "obscure[s] lifetime to the callers, or worse intermingle[s] two scopes together in unpredictable ways" — it "prevents the callers ... from specifying a deadline, requesting cancellation, and attaching metadata on a per-call basis" (Contexts and structs). The only sanctioned exception is retrofitting an existing API for backwards compatibility, and even then "first consider duplicating your functions" (Contexts and structs). The containedctx linter (go-tooling-and-static-analysis) finds a context.Context smuggled into a struct.


2. Where Contexts Come From: Background and TODO

You do not invent a context mid-call-chain — you thread the caller's. A root context is created only at an entry point: context.Background() "returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests" (context docs). When you genuinely don't have one yet, use context.TODO() — "Code should use context.TODO when it's unclear which Context to use or it is not yet available" (context docs). And never substitute nil: "Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure" (context docs).

// WRONG — minting a fresh root deep in a handler discards the request's deadline/cancellation
func (s *Server) handle(req *Request) error {
	return s.process(context.Background(), req) // the caller's ctx is now unreachable
}

// RIGHT — accept and thread the caller's ctx; Background() lives only at main/init/test entry
func (s *Server) handle(ctx context.Context, req *Request) error {
	return s.process(ctx, req)
}

context.Background() "should primarily be used in entrypoint functions (like main or init), not in library code mid-callchain" (Google Style Guide — Decisions).


3. Derive a Child, and ALWAYS defer cancel()

To add cancellation or a deadline, derive a child with WithCancel, WithTimeout, or WithDeadline. Each returns a cancel function you are obligated to call — on every path, including the success path and the timeout variants. "Failing to call the CancelFunc leaks the child and its children until the parent is canceled" (context docs). The idiom is defer cancel() on the line after the derivation:

// WRONG — no cancel(): the timer/goroutine behind the deadline leaks until parent dies
ctx, _ := context.WithTimeout(parent, 2*time.Second)
return doWork(ctx)

// RIGHT — derive, defer cancel immediately, then use ctx
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
return doWork(ctx)

Calling cancel even after the work succeeds is correct and required — it releases the resources early and is a no-op if cancellation already fired. go vet enforces this: its lostcancel analyzer reports "the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak" when you drop it into _ or let it go out of scope. The RIGHT snippet above passes go vet clean; the WRONG one does not. Wiring go vet into CI is owned by go-tooling-and-static-analysis.


4. Check Done() / Err() in Loops and Before Expensive Work

Holding a context does nothing unless you observe it. In a long loop or before a costly step, check for cancellation and return the context's error. Done() "returns a channel that acts as a cancellation signal to functions running on behalf of the Context: when the channel is closed, the functions should abandon their work and return" (Go blog: Context).

// RIGHT — abandon work the moment the caller cancels or the deadline passes
func process(ctx context.Context, items []Item) error {
	for _, it := range items {
		select {
		case <-ctx.Done():
			return ctx.Err() // Canceled or DeadlineExceeded
		default:
		}
		if err := handle(ctx, it); err != nil { // pass ctx onward, too
			return err
		}
	}
	return nil
}

A Context "is safe for simultaneous use by multiple goroutines. Code can pass a single Context to any number of goroutines and cancel that Context to signal all of them" (Go blog: Context) — one cancel fans out to every worker watching Done(). The select-on-Done() mechanics (default cases, nil-channel tricks, combining channels) are owned by go-channels-select; who stops the goroutine is owned by go-concurrency-goroutines.


5. Attach a Reason: WithCancelCause / Cause and WithDeadlineCause

A bare cancel tells a worker to stop but not why. WithCancelCause (Go 1.20) "returns a CancelCauseFunc that takes an error as the cancellation cause", and context.Cause(ctx) "returns the non-nil error explaining why a context was canceled" (context docs). ctx.Err() still returns the plain Canceled/DeadlineExceeded sentinel; Cause returns the richer reason.

ctx, cancel := context.WithCancelCause(parent)
// ... somewhere a worker fails fatally:
cancel(fmt.Errorf("upstream feed closed: %w", err))
// ... another goroutine sees the reason:
<-ctx.Done()
log.Printf("stopping: %v", context.Cause(ctx)) // the rich cause, not just "context canceled"

For deadlines, WithDeadlineCause and WithTimeoutCause (Go 1.21) "provide a way to set a context cancellation cause when a deadline or timer expires. The cause may be retrieved with the Cause function" (Go 1.21 release notes). Gate these on the module's go directive (go-version-feature-map).


6. Decouple with WithoutCancel and AfterFunc (Go 1.21)

Sometimes a child task must outlive the request that triggered it (a fire-and-forget cleanup or audit write). WithoutCancel (Go 1.21) "returns a copy of a context that is not canceled when the original context is canceled" (Go 1.21 release notes) — it keeps the values but drops the deadline and cancellation. And AfterFunc "registers a function to run after a context has been cancelled" (Go 1.21 release notes); it "calls f in its own goroutine after ctx is canceled" and returns a stop function to deregister it (context docs). Reach for these deliberately — the default is that a child does honor its parent's cancellation.


7. Context Values: Request-Scoped Data Only, Behind an Unexported Key

WithValue is the most-abused part of the package. The rule: "Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions" (context docs). Good values are request IDs, auth tokens, and trace spans — things that cross boundaries. A function's actual inputs are parameters, not smuggled context values. And the key must be an unexported type, not a string: "packages should define keys as an unexported type to avoid collisions" (context docs).

// WRONG — string key collides across packages; using ctx to pass a required arg
ctx = context.WithValue(ctx, "user", u)     // any other pkg's "user" key clobbers this
ctx = context.WithValue(ctx, "pageSize", 50) // pageSize is a parameter, not request scope

// RIGHT — unexported key type, request-scoped value, typed accessors
type userKey struct{}
func WithUser(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userKey{}, u)
}
func UserFrom(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userKey{}).(*User)
	return u, ok
}

The unexported userKey{} type is unreachable from other packages, so two libraries can each stash a "user" without colliding. Expose typed WithUser/UserFrom helpers rather than letting callers touch raw keys.


8. Canceled and DeadlineExceeded Are Sentinels — Match With errors.Is

A cancelled context's Err() is one of two sentinel values: Canceled is "the error returned by Context.Err when the context is canceled for some reason other than its deadline passing", and DeadlineExceeded is "the error returned by Context.Err when the context is canceled due to its deadline passing" (context docs). Because they are sentinels, you match them through a wrap chain with errors.Is, never by string:

if err := callBackend(ctx); err != nil {
	if errors.Is(err, context.DeadlineExceeded) {
		return fmt.Errorf("backend timed out: %w", err)
	}
	if errors.Is(err, context.Canceled) {
		return err // caller went away; nothing to add
	}
	return fmt.Errorf("backend: %w", err)
}

The full %w-wrapping and errors.Is/As discipline is owned by go-error-handling; this skill only notes that these two values are sentinels and must be matched as such.


9. Who Suffers When Context Is Done Badly

The victim is never the author at write time:

  • The on-call engineer paged at 3am: a WithTimeout whose cancel was dropped leaked a timer and a goroutine per request, and under load the process exhausted memory — exactly the backlog the blog warns of, where "your process could backlog and exhaust its resources (like memory)" (Contexts and structs). go vet's lostcancel would have caught it in CI.
  • The teammate who can't add a per-call deadline because ctx was frozen into a struct field at construction time, so every call inherits one request's cancellation scope.
  • The next maintainer who debugs a value that silently vanished because two packages used the string key "user" and one overwrote the other — the collision an unexported key type exists to prevent.

Per-call cancellation, an always-called cancel, and an unexported key are empathy for whoever runs this code in production.


10. Rules and Sources

RuleSource
ctx is the first parameter, named ctx; never a struct field"Do not store Contexts inside a struct type ... The Context should be the first parameter, typically named ctx" (context docs); "Do not add a context member to a struct type" (Google)
Root only at entry points; thread the caller's ctx; never pass nil"typically used by the main function, initialization, and tests"; "Do not pass a nil Context ... Pass context.TODO if you are unsure" (context docs)
Derive a child and ALWAYS defer cancel()"Failing to call the CancelFunc leaks the child and its children until the parent is canceled" (context docs)
Check Done()/Err() in loops and before costly work"when the channel is closed, the functions should abandon their work and return" (Go blog)
Attach a reason with WithCancelCause/Cause (1.20), WithDeadlineCause (1.21)"returns the non-nil error explaining why a context was canceled" (context docs); (Go 1.21)
WithoutCancel / AfterFunc (1.21) to decouple deliberately"a copy of a context that is not canceled when the original is canceled"; "registers a function to run after a context has been cancelled" (Go 1.21)
Values for request-scoped data only, behind an unexported key"only for request-scoped data that transits processes and APIs, not for passing optional parameters"; "define keys as an unexported type to avoid collisions" (context docs)
Canceled/DeadlineExceeded are sentinels — match with errors.Is"the error returned by Context.Err when the context is canceled" (context docs)

11. Routing to Related Skills

  • go-idiomatic-discipline — the policy root; this skill is the cancellation-and-propagation depth behind "never start a goroutine you can't stop."
  • go-concurrency-goroutineswho stops the goroutine; a ctx is how you stop it, but goroutine ownership/lifetime lives there.
  • go-channels-select — the select-on-Done() mechanics: default cases, nil-channel disabling, combining a Done() channel with others.
  • go-error-handlingcontext.Canceled/DeadlineExceeded as sentinels; %w wrapping and errors.Is/As depth.
  • go-tooling-and-static-analysisgo vet's lostcancel (missing cancel()) and the containedctx linter (ctx in a struct) in CI.
  • go-version-feature-map — which of WithCancelCause (1.20), WithoutCancel/AfterFunc/WithDeadlineCause (1.21) the module's go directive permits.

12. Reference Files

High-frequency context anti-patterns in LLM-generated Go, each with wrong/right code and citations:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.