Note
This project was 99% written by AI (Claude Code).
A Go linter that checks goroutine context propagation.
goroutinectx detects cases where a context.Context is available in function parameters but not properly passed to downstream calls that should receive it.
Using go install
go install github.com/mpyw/goroutinectx/cmd/goroutinectx@latest
goroutinectx ./...Using go tool (Go 1.24+)
# Add to go.mod as a tool dependency
go get -tool github.com/mpyw/goroutinectx/cmd/goroutinectx@latest
# Run via go tool
go tool goroutinectx ./...Using go run
go run github.com/mpyw/goroutinectx/cmd/goroutinectx@latest ./...Caution
To prevent supply chain attacks, pin to a specific version tag instead of @latest in CI/CD pipelines (e.g., @v0.7.5).
import "github.com/mpyw/goroutinectx"
func main() {
singlechecker.Main(goroutinectx.Analyzer)
}See singlechecker for details.
Or use it with multichecker alongside other analyzers.
Not currently integrated with golangci-lint. PRs welcome if someone wants to add it, but not actively pursuing integration.
Detects goroutines that don't propagate context:
func handler(ctx context.Context) {
// Bad: goroutine doesn't use ctx
go func() {
doSomething()
}()
// Good: goroutine uses ctx
go func() {
doSomething(ctx)
}()
}Important: Each goroutine must directly reference the context in its own function body. Context usage in nested closures doesn't count:
func handler(ctx context.Context) {
// Bad: ctx is only used in the nested closure, not in the goroutine itself
go func() {
go func() {
doSomething(ctx)
}()
}()
// Good: goroutine directly references ctx (even if not "using" it)
go func() {
_ = ctx // Explicit acknowledgment of context
go func() {
doSomething(ctx)
}()
}()
}This design ensures every goroutine explicitly acknowledges context propagation. If your goroutine doesn't need to use context directly but spawns nested goroutines that do, add _ = ctx to signal intentional propagation.
Detects errgroup.Group.Go closures that don't use context:
func handler(ctx context.Context) {
g := new(errgroup.Group)
// Bad: closure doesn't use ctx
g.Go(func() error {
return doSomething()
})
// Good: closure uses ctx
g.Go(func() error {
return doSomething(ctx)
})
}sync.WaitGroup (Go 1.25+)
Detects sync.WaitGroup.Go closures that don't use context:
func handler(ctx context.Context) {
var wg sync.WaitGroup
// Bad: closure doesn't use ctx
wg.Go(func() {
doSomething()
})
// Good: closure uses ctx
wg.Go(func() {
doSomething(ctx)
})
}Detects conc API calls where closures don't use context:
func handler(ctx context.Context) {
p := pool.New()
// Bad: closure doesn't use ctx
p.Go(func() {
doSomething()
})
// Good: closure uses ctx
p.Go(func() {
doSomething(ctx)
})
}Supported APIs:
conc.Pool.Go,conc.WaitGroup.Gopool.Pool.Go,pool.ResultPool[T].Go,pool.ContextPool.Go,pool.ResultContextPool[T].Gopool.ErrorPool.Go,pool.ResultErrorPool[T].Gostream.Stream.Goiter.ForEach,iter.ForEachIdx,iter.Map,iter.MapErriter.Iterator.ForEach,iter.Iterator.ForEachIdxiter.Mapper.Map,iter.Mapper.MapErr
gotask (requires -goroutine-deriver)
Detects gotask calls where task functions don't call the context deriver. Since tasks run as goroutines, they need to call the deriver function (e.g., apm.NewGoroutineContext) inside their body - there's no way to wrap the context at the call site.
Supported functions:
gotask.DoAllgotask.DoAllFnsgotask.DoAllSettledgotask.DoAllFnsSettledgotask.DoRacegotask.DoRaceFnsgotask.Task.DoAsyncgotask.CancelableTask.DoAsync
func handler(ctx context.Context) {
// Bad: task function doesn't call deriver
_ = gotask.DoAllFnsSettled(
ctx,
func(ctx context.Context) error {
return doSomething(ctx) // ctx is NOT derived!
},
)
// Good: task function calls deriver
_ = gotask.DoAllFnsSettled(
ctx,
func(ctx context.Context) error {
ctx = apm.NewGoroutineContext(ctx) // Properly derived
return doSomething(ctx)
},
)
}For gotask.Task.DoAsync and gotask.CancelableTask.DoAsync, the context argument must contain a deriver call:
func handler(ctx context.Context) {
task := gotask.NewTask(func(ctx context.Context) error {
return nil
})
// Bad: ctx is not derived
task.DoAsync(ctx, errChan)
// Good: ctx is derived
task.DoAsync(apm.NewGoroutineContext(ctx), errChan)
}Note: This checker only activates when -goroutine-deriver is set.
Suppress warnings for a specific line:
func handler(ctx context.Context) {
//goroutinectx:ignore - intentionally not passing context
go func() {
backgroundTask()
}()
}The comment can be on the same line or the line above.
You can specify which checker(s) to ignore:
func handler(ctx context.Context) {
//goroutinectx:ignore goroutine - only ignore goroutine checker
go func() {
backgroundTask()
}()
//goroutinectx:ignore goroutine,errgroup - ignore multiple checkers
g.Go(func() error {
return backgroundTask()
})
}Available checker names:
goroutine-go func()statementsgoroutinederive- goroutine derive function requirementwaitgroup-sync.WaitGroup.Gocallserrgroup-errgroup.Group.Gocallsconc- conc library checksspawner- spawner directive checksspawnerlabel- spawner label requirementgotask- gotask library checks
The analyzer reports unused //goroutinectx:ignore directives. If an ignore directive doesn't suppress any warning, it will be flagged as unused. This helps keep your codebase clean from stale ignore comments.
Mark a function as one that spawns goroutines with its func arguments. The analyzer will check that func arguments passed to marked functions properly use context:
//goroutinectx:spawner
func runAsync(g *errgroup.Group, fn func() error) {
g.Go(fn)
}
func handler(ctx context.Context) {
g := new(errgroup.Group)
// Bad: func argument doesn't use ctx
runAsync(g, func() error {
return doSomething()
})
// Good: func argument uses ctx
runAsync(g, func() error {
return doSomething(ctx)
})
}This is useful for wrapper functions that abstract away goroutine spawning patterns.
Require goroutines to call a specific function to derive context. Useful for APM libraries like New Relic.
# Single deriver - require apm.NewGoroutineContext() in goroutines
goroutinectx -goroutine-deriver='github.com/my-example-app/telemetry/apm.NewGoroutineContext' ./...
# AND (plus) - require BOTH txn.NewGoroutine() AND newrelic.NewContext()
goroutinectx -goroutine-deriver='github.com/newrelic/go-agent/v3/newrelic.Transaction.NewGoroutine+github.com/newrelic/go-agent/v3/newrelic.NewContext' ./...
# Mixed AND/OR - (txn.NewGoroutine AND NewContext) OR apm.NewGoroutineContext
goroutinectx -goroutine-deriver='github.com/newrelic/go-agent/v3/newrelic.Transaction.NewGoroutine+github.com/newrelic/go-agent/v3/newrelic.NewContext,github.com/my-example-app/telemetry/apm.NewGoroutineContext' ./...Format:
pkg/path.Funcfor functionspkg/path.Type.Methodfor methods,(comma) for OR - at least one group must be satisfied+(plus) for AND - all functions in the group must be called
Tip
When both parent and child goroutines require instrumentation inheritance (e.g., New Relic Go Agent), you need to call Transaction.NewGoroutine and NewContext:
go func() {
txn := newrelic.FromContext(ctx).NewGoroutine()
ctx := newrelic.NewContext(context.Background(), txn)
doSomething(ctx)
}()It's common to define a wrapper function that takes context.Context and returns context.Context:
// apm.NewGoroutineContext derives context for goroutine instrumentation
func NewGoroutineContext(ctx context.Context) context.Context {
txn := newrelic.FromContext(ctx)
if txn == nil {
return ctx
}
return newrelic.NewContext(ctx, txn.NewGoroutine())
}See also: New Relic Go Agent 完全理解・実践導入ガイド - Zenn (in Japanese)
Treat additional types as context carriers (like context.Context). Useful for web frameworks that have their own context types.
# Treat echo.Context as a context carrier
goroutinectx -context-carriers='github.com/labstack/echo/v4.Context' ./...
# Multiple carriers (comma-separated)
goroutinectx -context-carriers='github.com/labstack/echo/v4.Context,github.com/urfave/cli/v2.Context' ./...Example with echo.Context and cli.Context.
When a function has a context carrier parameter, goroutinectx will check that it's properly propagated to goroutines and other APIs.
Mark external package functions as spawners. This is the flag-based alternative to //goroutinectx:spawner directive for functions you don't control.
# Single external spawner
goroutinectx -external-spawner='github.com/example/workerpool.Pool.Submit' ./...
# Multiple external spawners (comma-separated)
goroutinectx -external-spawner='github.com/example/workerpool.Pool.Submit,github.com/example/workerpool.Run' ./...Format:
pkg/path.Funcfor package-level functionspkg/path.Type.Methodfor methods
When an external spawner is called, goroutinectx checks that func arguments properly use context.
Most checkers are enabled by default. Use these flags to enable or disable specific checkers:
Available flags:
-goroutine(default: true)-waitgroup(default: true)-errgroup(default: true)-conc(default: true) - Check conc APIs:conc.Pool.Go,conc.WaitGroup.Gopool.Pool.Go,pool.ResultPool[T].Go,pool.ContextPool.Go,pool.ResultContextPool[T].Gopool.ErrorPool.Go,pool.ResultErrorPool[T].Gostream.Stream.Goiter.ForEach,iter.ForEachIdx,iter.Map,iter.MapErriter.Iterator.ForEach,iter.Iterator.ForEachIdxiter.Mapper.Map,iter.Mapper.MapErr
-spawner(default: true)-spawnerlabel(default: false) - Check that spawner functions are properly labeled-gotask(default: true, requires-goroutine-deriver)
| Flag | Default | Description |
|---|---|---|
-test |
true |
Analyze test files (*_test.go) — built-in driver flag |
Generated files (containing // Code generated ... DO NOT EDIT.) are always excluded and cannot be opted in.
# Exclude test files from analysis
goroutinectx -test=false ./...When enabled, checks that functions calling spawn methods with func arguments have the //goroutinectx:spawner directive:
// Bad: calls errgroup.Group.Go() with func argument but missing directive
func runTask(task func() error) { // Warning: should have //goroutinectx:spawner
g := new(errgroup.Group)
g.Go(task)
_ = g.Wait()
}
// Good: properly labeled
//goroutinectx:spawner
func runTask(task func() error) {
g := new(errgroup.Group)
g.Go(task)
_ = g.Wait()
}Also warns about unnecessary labels on functions that don't spawn and have no func parameters:
// Bad: unnecessary directive (no spawn calls, no func parameters)
//goroutinectx:spawner
func simpleHelper() { // Warning: unnecessary //goroutinectx:spawner
fmt.Println("hello")
}- Zero false positives - Prefer missing issues over false alarms
- Type-safe analysis - Uses
go/typesfor accurate detection - Nested function support - Correctly tracks context through closures
- Architecture - Technical specification and design decisions
- Tutorial - Step-by-step learning guide
- CLAUDE.md - AI assistant guidance for development
- zerologlintctx - Zerolog context propagation linter
- ctxweaver - Code generator for context-aware instrumentation
- gormreuse - GORM instance reuse linter
- contextcheck - Detects
context.Background/context.TODOusage and missing context parameters
goroutinectx is complementary to contextcheck:
contextcheckwarns about creating new contexts when one should be propagatedgoroutinectxwarns about not using an available context in specific APIs
MIT