Go Concurrency skill for Codex

Every goroutine has an owner and an exit. Codex starts no goroutine without an owner that waits for it, threads context cancellation through every blocking call, and bounds fan-out with errgroup limits or fixed worker pools. It chooses mutex, atomic or channel per piece of state, reads goroutine dumps before guessing at hangs, and proves fixes with go test -race, goleak and synctest fake clocks.

  • Languages
  • $go-concurrency
  • 203 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- go-concurrency

Installs to ~/.agents/skills/go-concurrency/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $go-concurrency, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and debugs concurrent Go - goroutine ownership and shutdown, context cancellation, errgroup with SetLimit, WaitGroup.Go, channel close rules, worker pools and pipelines, mutexes versus channels, sync.Once and atomics, and finding races and leaks with go test -race, testing/synctest, goleak and goroutine profiles. Use when Go code deadlocks, leaks goroutines, has data races or flaky timing tests, ignores cancellation, or when a user asks to parallelize work with bounded concurrency.

Use it when

  • Go code deadlocks, leaks goroutines or panics on concurrent map writes
  • Work should run in parallel with a concurrency limit and error cancellation
  • Shutdown or request cancellation does not stop background work
  • Tests that involve timers or goroutines are flaky

Use something else when

  • You need CPU or heap profiling to find what is slow: use go-pprof
  • The question is HTTP server structure and graceful shutdown wiring: use go-http-services

How to check the result

go test -race -count=5 passes, goleak reports nothing, and a test shows that cancelling the context stops all work within a bounded time.

The mistake it prevents

Starting goroutines that block forever on a channel nobody reads after an early return, leaking one goroutine per request.

~/.agents/skills/go-concurrency/SKILL.md

--- name: go-concurrency description: Writes and debugs concurrent Go - goroutine ownership and shutdown, context cancellation, errgroup with SetLimit, WaitGroup.Go, channel close rules,… license: MIT ---

Done well means: every goroutine has an owner that knows when it ends, cancellation flows through context.Context, concurrency is bounded, shared state has one clear guard, and the tests pass under -race repeatedly.

Before changing anything

  • Read AGENTS.md and go.mod. WaitGroup.Go and stable testing/synctest need Go 1.25; per-iteration loop variables need 1.22. Check whether golang.org/x/sync is already a dependency before adding it (adding needs network).
  • Run the race detector on the current code to get a baseline: go test -race -count=3 ./... (needs cgo on some platforms; set CGO_ENABLED=1).
  • For a hang, get a goroutine dump before guessing: send SIGQUIT (Ctrl-) to the process, or hit /debug/pprof/goroutine?debug=2 if net/http/pprof is mounted.

Rules that prevent most bugs

  1. Never start a goroutine without knowing how it stops. The function that starts it waits for it (WaitGroup, errgroup, done channel) or hands that duty to a clear owner.
  2. Pass ctx context.Context as the first parameter to anything that blocks; check ctx.Done() in every loop and select.
  3. Only the sender closes a channel, and only when no more sends can happen. Receivers never close. With multiple senders, close after a WaitGroup says they are all done.
  4. Bound concurrency: errgroup.SetLimit, a semaphore, or a fixed worker pool.
  5. One guard per piece of shared state: a mutex next to the fields it protects, or a single goroutine that owns the state and talks over channels. Never both.

Choosing a tool

NeedUse
Run N tasks, first error cancels the resterrgroup.WithContext + g.SetLimit(n)
Run N tasks, no errors, just waitsync.WaitGroup with wg.Go(func(){...}) (1.25)
Protect a map or struct fieldssync.Mutex / sync.RWMutex (RW only if reads dominate and are slow)
Counter or flagatomic.Int64, atomic.Bool
Lazy one-time initsync.OnceValue(func() T) / sync.OnceValues (1.21)
Hand off work or results between goroutinesChannels
Limit concurrent access to a resourceBuffered channel as semaphore or x/sync/semaphore (weighted)
Cache many readers, rare writers, disjoint keyssync.Map only for that pattern; otherwise a mutex map
Deduplicate concurrent identical callsx/sync/singleflight

errgroup with bounded fan-out

import "golang.org/x/sync/errgroup"

func FetchAll(ctx context.Context, ids []string) ([]Item, error) {
    items := make([]Item, len(ids))      // each goroutine writes its own index: no lock needed
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(8)
    for i, id := range ids {
        g.Go(func() error {
            it, err := fetch(ctx, id)
            if err != nil {
                return fmt.Errorf("fetch %s: %w", id, err)
            }
            items[i] = it
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return items, nil
}
  • The derived ctx is cancelled when any task fails; tasks must use it, not the outer one.
  • g.Go blocks when the limit is reached, which gives natural backpressure.
  • Collect results by index into a pre-sized slice, or send them on a channel; never append to a shared slice without a lock.

Worker pool with a results channel

func Process(ctx context.Context, jobs []Job, workers int) <-chan Result {
    in := make(chan Job)
    out := make(chan Result)

    go func() {                           // producer owns and closes `in`
        defer close(in)
        for _, j := range jobs {
            select {
            case in <- j:
            case <-ctx.Done():
                return
            }
        }
    }()

    var wg sync.WaitGroup
    for range workers {
        wg.Go(func() {
            for j := range in {
                select {
                case out <- handle(ctx, j):
                case <-ctx.Done():
                    return
                }
            }
        })
    }
    go func() { wg.Wait(); close(out) }()  // closer waits for every sender
    return out
}

The caller must drain out or cancel ctx; otherwise workers block forever on send.

Context patterns

NeedAPI
Deadline for a call chainctx, cancel := context.WithTimeout(ctx, 2*time.Second); defer cancel()
Record why it was cancelledcontext.WithCancelCause, context.Cause(ctx)
Run cleanup when ctx endsstop := context.AfterFunc(ctx, fn)
Background work that outlives the request but keeps valuescontext.WithoutCancel(ctx) (still bound it with its own timeout)
Graceful shutdown on SIGINT/SIGTERMsignal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)

Always defer cancel(); go vet reports lost cancel functions. Do not store contexts in structs.

Shared state with a mutex

type Counter struct {
    mu     sync.Mutex
    counts map[string]int     // guarded by mu
}

func (c *Counter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.counts == nil {
        c.counts = make(map[string]int)
    }
    c.counts[key]++
}

Keep critical sections short; never call out to network or user callbacks while holding a lock. Structs containing a mutex must not be copied (go vet copylocks); pass pointers.

Diagnosing

SymptomLikely causeCheck
fatal error: all goroutines are asleep - deadlock!Unbuffered send with no receiver, or waiting on a WaitGroup that never reaches zeroRead the dump: which goroutine is blocked on what
Memory grows, goroutine count growsLeaked goroutines blocked on send/receive or ignoring ctxruntime.NumGoroutine() over time; pprof goroutine profile grouped by stack
concurrent map writes panicUnguarded mapAdd a mutex; run -race to find others
Flaky test depending on time.SleepTiming assumptionstesting/synctest with a fake clock
Throughput flat as workers increaseShared lock contention, or I/O limit elsewhereMutex profile: runtime.SetMutexProfileFraction, then pprof

Testing concurrent code

func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }   // go.uber.org/goleak

func TestDebounce(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {      // Go 1.25: fake clock inside the bubble
        d := NewDebouncer(100 * time.Millisecond)
        d.Trigger()
        time.Sleep(150 * time.Millisecond)     // returns instantly; time is virtual
        synctest.Wait()
        if d.Fired() != 1 {
            t.Fatalf("fired %d times", d.Fired())
        }
    })
}
  • Run go test -race -count=10 -run TestName ./pkg on any test touching concurrency; races are probabilistic.
  • -race only finds races that actually execute. Exercise the concurrent paths in tests.

Gotchas

  • Sending on a closed channel panics; receiving from a closed channel returns the zero value immediately (use v, ok := <-ch). A nil channel blocks forever, which is useful for disabling a select case.
  • time.After in a long-running select loop allocates a timer per iteration; use a time.Ticker or reset a time.Timer.
  • GOMAXPROCS is cgroup-aware from Go 1.25; on older versions in containers, use go.uber.org/automaxprocs.
  • Capturing loop variables in goroutines is safe from Go 1.22 only if go.mod declares 1.22+.
  • Unrecovered panics in any goroutine crash the whole program; errgroup does not recover them. Recover at goroutine boundaries in long-running servers.
  • More goroutines is not faster for CPU-bound work beyond GOMAXPROCS; measure.

Verification

  • go test -race -count=5 ./... passes; goleak (if used) reports no leaked goroutines.
  • go vet ./... clean (copylocks, lostcancel).
  • A cancellation test proves shutdown: cancel the context and assert the function returns within a bound and goroutine count returns to baseline.
  • For performance claims, a benchmark (go test -bench . -cpu 1,4,8) shows the speedup.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Go Concurrency"
  short_description: "Every goroutine has an owner and an exit"
  default_prompt: "$go-concurrency Process these 50k records in parallel with at most 16 goroutines"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem