Skip to content

mpyw/goroutinectx

Repository files navigation

goroutinectx

Go Reference CI Codecov Go Report Card License: MIT

Note

This project was 99% written by AI (Claude Code).

A Go linter that checks goroutine context propagation.

Overview

goroutinectx detects cases where a context.Context is available in function parameters but not properly passed to downstream calls that should receive it.

Installation & Usage

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).

As a Library

import "github.com/mpyw/goroutinectx"

func main() {
    singlechecker.Main(goroutinectx.Analyzer)
}

See singlechecker for details.

Or use it with multichecker alongside other analyzers.

golangci-lint

Not currently integrated with golangci-lint. PRs welcome if someone wants to add it, but not actively pursuing integration.

What It Checks

goroutines

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:

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:

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.

Directives

//goroutinectx:ignore

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.

Checker-Specific Ignore

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() statements
  • goroutinederive - goroutine derive function requirement
  • waitgroup - sync.WaitGroup.Go calls
  • errgroup - errgroup.Group.Go calls
  • conc - conc library checks
  • spawner - spawner directive checks
  • spawnerlabel - spawner label requirement
  • gotask - gotask library checks

Unused Ignore Detection

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.

//goroutinectx:spawner

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.

Flags

-goroutine-deriver

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.Func for functions
  • pkg/path.Type.Method for 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)

-context-carriers

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.

-external-spawner

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.Func for package-level functions
  • pkg/path.Type.Method for methods

When an external spawner is called, goroutinectx checks that func arguments properly use context.

Checker Enable/Disable Flags

Most checkers are enabled by default. Use these flags to enable or disable specific checkers:

Available flags:

File Filtering

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 ./...

-spawnerlabel

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")
}

Design Principles

  1. Zero false positives - Prefer missing issues over false alarms
  2. Type-safe analysis - Uses go/types for accurate detection
  3. Nested function support - Correctly tracks context through closures

Documentation

  • Architecture - Technical specification and design decisions
  • Tutorial - Step-by-step learning guide
  • CLAUDE.md - AI assistant guidance for development

Related Tools

goroutinectx is complementary to contextcheck:

  • contextcheck warns about creating new contexts when one should be propagated
  • goroutinectx warns about not using an available context in specific APIs

License

MIT