Skip to content
Back to skills

Golang Concurrency

ASecurity

This skill should be used when the user asks to "review this Go code for goroutine leaks", "should I use a channel or a mutex", "use sync.WaitGroup vs errgroup", "audit this Go code for race conditions", "write a Go pipeline with fan-out/fan-in", "add a bounded worker pool in Go", or is choosing between sync primitives, debugging a goroutine leak, or designing a concurrent pipeline. For the one-paragraph "use errgroup for fanning out work" default in ordinary service code, use go-service-idio...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsgotestingdebuggingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill golang-concurrency --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Golang Concurrency?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Golang Concurrency
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-golang-concurrency/badge)](https://www.skillsdirectory.com/skills/shennawardana23-golang-concurrency)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: golang-concurrency
description: This skill should be used when the user asks to "review this Go code for goroutine leaks", "should I use a channel or a mutex", "use sync.WaitGroup vs errgroup", "audit this Go code for race conditions", "write a Go pipeline with fan-out/fan-in", "add a bounded worker pool in Go", or is choosing between sync primitives, debugging a goroutine leak, or designing a concurrent pipeline. For the one-paragraph "use errgroup for fanning out work" default in ordinary service code, use go-service-idioms instead; this skill is for the deeper decision (which primitive, how the goroutine exits, leak/race auditing) once concurrency itself is the question.
license: Apache-2.0
metadata:
  version: "0.1.0"
  category: "go"
---

# Go Concurrency

Go's concurrency model is a liability until proven necessary: every
goroutine you spawn is a resource with an owner, an exit condition, and a
failure mode you must account for. Reach for this skill once concurrency
itself is the design question — which primitive to use, whether a
goroutine can leak, whether shared state is actually protected — not for
the routine "fan this out" case, which `go-service-idioms` already covers
with `errgroup`.

## The one question to ask before spawning a goroutine

**How does it exit?** Every goroutine needs one of: a `context.Context`
whose cancellation it observes in a `select`, a channel it reads until
close, or a `sync.WaitGroup`/`errgroup` the caller waits on. A goroutine
with none of these is a leak the moment its one caller stops waiting for
it — `runtime.NumGoroutine()` climbing over time in production is the
symptom, not the cause.

## Choosing a primitive

| Scenario | Use | Why |
|---|---|---|
| Passing data between goroutines, transferring ownership | channel | Communicates "you own this now," not just "here's a pointer" |
| Waiting for goroutines, no error to collect | `sync.WaitGroup`, or Go 1.25+ `wg.Go(f)` | `wg.Go` starts the goroutine and calls `Add`/`Done` for you — no more forgetting `Add` before `go` |
| Waiting + collecting the first error | `golang.org/x/sync/errgroup` | `g.Go(func() error {...}); g.Wait()` returns the first non-nil error |
| Waiting + canceling siblings on first error | `errgroup.WithContext(ctx)` | The returned `ctx` cancels for every `g.Go` call the moment one returns an error |
| Bounding concurrency without a hand-rolled semaphore | `errgroup.Group.SetLimit(n)` | Same `Go`/`Wait` API, caps in-flight goroutines |
| Protecting a shared struct's fields | `sync.Mutex` / `sync.RWMutex` | Simple critical sections; never hold one across a blocking I/O call |
| A single counter or flag | `sync/atomic` typed atomics (`atomic.Int64`, `atomic.Bool`, Go 1.19+) | Lock-free, cheaper than a mutex for one word |
| A map read far more than it's written | `sync.Map` | Optimized for read-heavy access; a plain `map` with concurrent read+write panics at runtime, it does not silently corrupt |
| Running init exactly once | `sync.Once` (or Go 1.21+ `sync.OnceFunc`/`OnceValue`) | Safe lazy init without a manual `initialized bool` + mutex |
| Deduplicating identical concurrent calls | `golang.org/x/sync/singleflight` | Collapses N simultaneous callers into one underlying call |

## Channels: rules that prevent real bugs

- **Only the sender closes a channel.** A receiver that closes it, or a
  second sender that closes an already-closed channel, panics.
- **Send copies or immutable values, not pointers to mutable data** — a
  pointer on a channel is shared memory with an extra step, which defeats
  the point of communicating by channel instead of by mutex.
- **Declare channel direction in signatures** (`chan<- T` for send-only,
  `<-chan T` for receive-only) so the compiler catches misuse at the call
  site instead of at runtime.
- **Always include `ctx.Done()` in a `select`** alongside channel
  operations in a loop; without it, a goroutine blocked on a channel that
  will never receive again outlives the caller that gave up on it.
- **Don't call `time.After` inside a hot loop** — each call allocates a
  new timer that isn't garbage-collected until it fires. Create one
  `time.NewTimer` outside the loop and call `Reset` inside it instead.

## Fan-out/fan-in and worker pools

Bound the number of workers to a fixed count reading from a shared input
channel rather than spawning one goroutine per item when the item count is
unbounded (user input, a queue, a directory walk):

```go
func process(ctx context.Context, items []Item, workers int) error {
    g, ctx := errgroup.WithContext(ctx)
    in := make(chan Item)

    g.Go(func() error {
        defer close(in)
        for _, it := range items {
            select {
            case in <- it:
            case <-ctx.Done():
                return ctx.Err()
            }
        }
        return nil
    })

    for i := 0; i < workers; i++ {
        g.Go(func() error {
            for it := range in {
                if err := handle(ctx, it); err != nil {
                    return err
                }
            }
            return nil
        })
    }
    return g.Wait()
}
```

This shape gives every stage a clear exit (`close(in)` from its single
sender, `ctx.Done()` in the feeder's select, `range` draining the channel
in each worker until close) — the three failure modes below are all
missing one of those.

## Gotchas

- `recover()` only catches a panic within the same goroutine — a
  goroutine started with `go func() {...}()` that panics and isn't
  recovered *inside that function* crashes the whole process, even if the
  rest of the program handles panics correctly elsewhere.
- `wg.Add` must happen before the `go` statement, not inside the spawned
  goroutine — `Wait` can return before a late `Add` ever registers,
  making the wait silently incomplete. `wg.Go(f)` (Go 1.25+) removes this
  class of bug entirely by doing `Add`/`go`/`Done` atomically.
- `sync.RWMutex.RLock` held by one goroutine while another calls `Lock`
  deadlocks if the first goroutine tries to upgrade its own `RLock` to
  `Lock` without releasing it first — never upgrade a read lock in place.
- Reusing a `sync.Pool` object without resetting it before `Put` leaks
  stale data into the next `Get` caller; always zero or reset before
  returning an object to the pool.
- `go test -race` only catches races that actually execute during that
  run — a race-free test run is not proof of a race-free program, only
  evidence for the code paths the test exercised. Run `-race` in CI on
  every PR, not just once.
- Track leaks in tests with `go.uber.org/goleak`; a leaked goroutine from
  one test can silently keep running during unrelated later tests in the
  same process and skew their timing or resource use.

## Real-world grounding

`sync.WaitGroup.Go` and the typed atomics under `sync/atomic` are
documented in the Go standard library (`go doc sync.WaitGroup`,
pkg.go.dev/sync); `golang.org/x/sync/errgroup` and `singleflight` are the
Go team's own supplementary concurrency packages, not a third-party
convention layered on top.

## Cross-references

- For the routine "fan this out with errgroup" default in ordinary
  service code, see `go-service-idioms`.
- For `context.Context` cancellation and timeout composition feeding the
  `select` statements above, see `golang-context`.
- For allocation-driven performance work once a concurrent path is
  correct, see `golang-patterns` and `golang-testing` (benchmarking).

Files in this skill

  • SKILL.md7.3 KB
  • evals/evals.json3.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.