返回 Skills
samber/cc-skills-golang· MIT 内容可用

golang-concurrency

Golang concurrency patterns. Use when writing or reviewing concurrent Go code involving goroutines, channels, select, locks, sync primitives, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Also triggers when you detect goroutine leaks, race conditions, channel ownership issues, or need to choose between channels and mutexes.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: golang-concurrency description: "Golang concurrency patterns. Use when writing or reviewing concurrent Go code involving goroutines, channels, select, locks, sync primitives, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Also triggers when you detect goroutine leaks, race conditions, channel ownership issues, or need to choose between channels and mutexes." user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber version: "1.1.5" openclaw: emoji: "⚡" homepage: https://github.com/samber/cc-skills-golang requires: bins: - go install: [] allowed-tools: Read Edit Write Glob Grep Bash(go:) Bash(golangci-lint:) Bash(git:*) Agent AskUserQuestion

Persona: You are a Go concurrency engineer. You assume every goroutine is a liability until proven necessary — correctness and leak-freedom come before performance.

Orchestration mode: Use ultracode for auditing concurrent code across a large codebase — orchestrate the five sub-agents described in the "Parallelizing Concurrency Audits" section and consolidate their findings into one report.

Modes:

  • Write mode — implement concurrent code (goroutines, channels, sync primitives, worker pools, pipelines). Follow the sequential instructions below.
  • Review mode — reviewing a PR's concurrent code changes. Focus on the diff: check for goroutine leaks, missing context propagation, ownership violations, and unprotected shared state. Sequential.
  • Audit mode — auditing existing concurrent code across a codebase. Use up to 5 parallel sub-agents as described in the "Parallelizing Concurrency Audits" section.

Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-concurrency skill takes precedence.

Go Concurrency Best Practices

Go's concurrency model is built on goroutines and channels. Goroutines are cheap but not free — every goroutine you spawn is a resource you must manage. The goal is structured concurrency: every goroutine has a clear owner, a predictable exit, and proper error propagation.

Core Principles

  1. Every goroutine must have a clear exit — without a shutdown mechanism (context, done channel, WaitGroup), they leak and accumulate until the process crashes
  2. Share memory by communicating — channels transfer ownership explicitly; mutexes protect shared state but make ownership implicit
  3. Send copies, not pointers on channels — sending pointers creates invisible shared memory, defeating the purpose of channels
  4. Only the sender closes a channel — closing from the receiver side panics if the sender writes after close
  5. Specify channel direction (chan<-, <-chan) — the compiler prevents misuse at build time
  6. Default to unbuffered channels — larger buffers mask backpressure; use them only with measured justification
  7. Always include ctx.Done() in select — without it, goroutines leak after caller cancellation
  8. Avoid repeated time.After in hot loops — each call allocates a timer and creates unnecessary churn; use time.NewTimer + Reset for long-running loops
  9. Track goroutine leaks in tests with go.uber.org/goleak

For detailed channel/select code examples, see Channels and Select Patterns.

Channel vs Mutex vs Atomic

ScenarioUseWhy
Passing data between goroutinesChannelCommunicates ownership transfer
Coordinating goroutine lifecycleChannel + contextClean shutdown with select
Protecting shared struct fieldssync.Mutex / sync.RWMutexSimple critical sections
Simple counters, flagssync/atomicLock-free, lower overhead
Many readers, few writers on a mapsync.MapOptimized for read-heavy workloads. Concurrent map read/write causes a hard crash
Caching expensive computationssync.Once / singleflightExecute once or deduplicate

WaitGroup vs errgroup

NeedUseWhy
Wait for goroutines, errors not neededsync.WaitGroupFire-and-forget
Wait + collect first errorerrgroup.GroupError propagation
Wait + cancel siblings on first errorerrgroup.WithContextContext cancellation on error
Wait + limit concurrencyerrgroup.SetLimit(n)Built-in worker pool

Sync Primitives Quick Reference

PrimitiveUse caseKey notes
sync.MutexProtect shared stateKeep critical sections short; never hold across I/O
sync.RWMutexMany readers, few writersNever upgrade RLock to Lock (deadlock)
sync/atomicSimple counters, flagsPrefer typed atomics (Go 1.19+): atomic.Int64, atomic.Bool
sync.MapConcurrent map, read-heavyNo explicit locking; use RWMutex+map when writes dominate
sync.PoolReuse temporary objectsAlways Reset() before Put(); reduces GC pressure
sync.OnceOne-time initializationGo 1.21+: OnceFunc, OnceValue, OnceValues
sync.WaitGroupWaiting for simple goroutinesGo 1.25+: prefer wg.Go(func(){ ... }) for fire-and-wait tasks that do not panic and do not need error propagation. For Go <1.25 use Add/Done. For errors/cancellation/limits, use errgroup with context.
x/sync/singleflightDeduplicate concurrent callsCache stampede prevention
x/sync/errgroupGoroutine group + errorsSetLimit(n) replaces hand-rolled worker pools

For detailed examples and anti-patterns, see Sync Primitives Deep Dive.

Concurrency Checklist

Before spawning a goroutine, answer:

  • How will it exit? — context cancellation, channel close, or explicit signal
  • Can I signal it to stop? — pass context.Context or done channel
  • Can I wait for it?sync.WaitGroup or errgroup
  • Who owns the channels? — creator/sender owns and closes
  • Should this be synchronous instead? — don't add concurrency without measured need

Pipelines and Worker Pools

For pipeline patterns (fan-out/fan-in, bounded workers, generator chains, Go 1.23+ iterators, samber/ro), see Pipelines and Worker Pools.

Parallelizing Concurrency Audits

When auditing concurrency across a large codebase, use up to 5 parallel sub-agents (Agent tool):

  1. Find all goroutine spawns (go func, go method) and verify shutdown mechanisms
  2. Search for mutable globals and shared state without synchronization
  3. Audit channel usage — ownership, direction, closure, buffer sizes
  4. Find time.After in loops, missing ctx.Done() in select, unbounded spawning
  5. Check mutex usage, sync.Map, atomics, and thread-safety documentation

Common Mistakes

MistakeFix
Fire-and-forget goroutineProvide stop mechanism (context, done channel)
Closing channel from receiverOnly the sender closes
time.After in hot loopReuse time.NewTimer + Reset
Missing ctx.Done() in selectAlways select on context to allow cancellation
Unbounded goroutine spawningUse errgroup.SetLimit(n) or semaphore
Sharing pointer via channelSend copies or immutable values
wg.Add inside goroutineCall Add before goWait may return early otherwise
Forgetting -race in CIAlways run go test -race ./...
Mutex held across I/OKeep critical sections short

Cross-References

  • -> See samber/cc-skills-golang@golang-performance skill for false sharing, cache-line padding, sync.Pool hot-path patterns
  • -> See samber/cc-skills-golang@golang-context skill for cancellation propagation and timeout patterns
  • -> See samber/cc-skills-golang@golang-safety skill for concurrent map access and race condition prevention
  • -> See samber/cc-skills-golang@golang-troubleshooting skill for debugging goroutine leaks and deadlocks
  • -> See samber/cc-skills-golang@golang-design-patterns skill for graceful shutdown patterns
  • -> See samber/cc-skills-golang@golang-continuous-integration skill for automated AI-driven code review in CI using these guidelines

Go 1.26 experimental goroutine leak profile

For Go 1.26 diagnostics, there is an experimental goroutine leak profile. It is useful for production-oriented leak investigation, but is gated by GOEXPERIMENT=goroutineleakprofile; do not rely on it as default stable behavior.

Typical usage when the experiment is enabled:

curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleak

Keep existing tools:

  • tests: go.uber.org/goleak
  • runtime count: runtime.NumGoroutine()
  • stack dump: /debug/pprof/goroutine?debug=2
  • race checks: go test -race ./...

References

附带文件

evals/evals.json
[
  {
    "id": 1,
    "name": "time-after-in-select-loop",
    "description": "Tests whether the model avoids time.After inside a select loop and uses time.NewTimer with Reset instead",
    "prompt": "I have a Go worker that reads from a channel and needs a 5-second inactivity timeout. If nothing arrives for 5 seconds, it should log and continue waiting. Write the select loop.",
    "trap": "Without the skill, the model uses time.After(5*time.Second) inside the for/select loop, creating repeated timer allocations/churn instead of reusing a timer",
    "assertions": [
      {"id": "1.1", "text": "Does NOT use time.After inside the loop body"},
      {"id": "1.2", "text": "Creates time.NewTimer (or time.NewTicker) outside the loop"},
      {"id": "1.3", "text": "Calls timer.Reset() after handling a message or timeout"},
      {"id": "1.4", "text": "Calls timer.Stop() (or defers it) to clean up the timer"},
      {"id": "1.5", "text": "For Go 1.23+, does not use the old stale-drain pattern; for Go <1.23 compatibility, drains before Reset when Stop reports a possible pending value"}
    ]
  },
  {
    "id": 2,
    "name": "channel-closing-ownership",
    "description": "Tests whether the model follows the rule that only the sender closes a channel, not the receiver",
    "prompt": "I have a producer goroutine that sends items to a channel and a consumer goroutine that reads from it. The consumer knows when it has received enough items and wants to signal completion. How should I close the channel to stop the producer?",
    "trap": "Without the skill, the model closes the channel from the consumer side, which panics if the producer writes after close",
    "assertions": [
      {"id": "2.1", "text": "Does NOT close the channel from the consumer/receiver side"},
      {"id": "2.2", "text": "Uses a separate signaling mechanism (done channel, context cancellation, or similar) for the consumer to tell the producer to stop"},
      {"id": "2.3", "text": "The producer is the one that closes the data channel (or it is closed by the channel creator/sender)"},
      {"id": "2.4", "text": "Explains the panic risk of closing a channel from the receiver side"},
      {"id": "2.5", "text": "The producer selects on the stop signal alongside its send operation"}
    ]
  },
  {
    "id": 3,
    "name": "waitgroup-add-placement",
    "description": "wg.Add before go statement; wg.Add(len(urls)) is preferred over per-goroutine Add(1) when count is known",
    "prompt": "Review this Go code and identify all WaitGroup issues:\n\n```go\nfunc ProcessURLs(urls []string) []Result {\n    var wg sync.WaitGroup\n    results := make([]Result, len(urls))\n\n    for i, url := range urls {\n        go func(i int, url string) {\n            wg.Add(1)\n            defer wg.Done()\n            results[i] = fetch(url)\n        }(i, url)\n    }\n\n    wg.Wait()\n    return results\n}\n```\n\nFix all issues. Also, is there a more efficient way to add to the WaitGroup when the number of goroutines is known upfront?",
    "trap": "The most obvious bug (wg.Add inside goroutine) is well-known. The model should also catch the second issue: wg.Add(len(urls)) before the loop is more efficient than N separate wg.Add(1) calls (single atomic update vs N atomic updates). The model likely fixes the placement but may not suggest the batch Add optimization.",
    "assertions": [
      {"id": "3.1", "text": "Moves wg.Add(1) BEFORE the go statement — wg.Add inside the goroutine races with wg.Wait(), which can return before all goroutines have registered"},
      {"id": "3.2", "text": "Suggests using wg.Add(len(urls)) before the loop as a more efficient alternative — a single atomic operation instead of N separate wg.Add(1) calls in the loop"},
      {"id": "3.3", "text": "Keeps defer wg.Done() inside the goroutine"},
      {"id": "3.4", "text": "Notes the race condition: wg.Wait() could return before some goroutines have called wg.Add(1), causing ProcessURLs to return with goroutines still running"}
    ]
  },
  {
    "id": 4,
    "name": "channel-direction-in-signatures",
    "description": "Tests whether the model specifies channel direction (send-only, receive-only) in function signatures",
    "prompt": "Write a Go pipeline with two functions: one that generates integers from a slice and sends them to a channel, and one that reads integers from a channel, doubles them, and sends to an output channel. Wire them together in main.",
    "trap": "Without the skill, the model uses bidirectional chan int in function parameters instead of chan<- and <-chan",
    "assertions": [
      {"id": "4.1", "text": "The generator function returns <-chan int (receive-only for callers)"},
      {"id": "4.2", "text": "The doubler function accepts <-chan int as input parameter"},
      {"id": "4.3", "text": "The doubler function returns <-chan int (receive-only for callers)"},
      {"id": "4.4", "text": "Internally, channels are created as bidirectional but exposed as directional through return types"},
      {"id": "4.5", "text": "The producer (generator) closes its output channel with defer close(out)"}
    ]
  },
  {
    "id": 5,
    "name": "unbuffered-channel-default",
    "description": "Tests whether the model defaults to unbuffered channels and requires justification for buffered ones",
    "prompt": "I need a channel to pass tasks from a dispatcher to a pool of workers in Go. Should I buffer it? If so, how large?",
    "trap": "Without the skill, the model suggests an arbitrary large buffer (e.g., 100 or 1000) without justification or discussion of backpressure",
    "assertions": [
      {"id": "5.1", "text": "Recommends starting with unbuffered (or very small buffer like 0 or 1) as the default"},
      {"id": "5.2", "text": "Explains that large buffers mask backpressure problems"},
      {"id": "5.3", "text": "States that buffer size should be based on measured need, not arbitrary choice"},
      {"id": "5.4", "text": "Does NOT suggest a large arbitrary buffer (e.g., 100, 1000) without explaining the tradeoffs"},
      {"id": "5.5", "text": "Mentions that buffered channels hide the problem of slow consumers"}
    ]
  },
  {
    "id": 6,
    "name": "send-copies-not-pointers",
    "description": "Tests whether the model sends copies (not pointers) through channels to avoid shared memory",
    "prompt": "I have a producer goroutine that creates Task structs and sends them to a channel for worker goroutines to process. The Task struct has fields like ID, Payload, and Status. Write the producer and consumer code.",
    "trap": "Without the skill, the model sends *Task pointers through the channel, creating invisible shared memory that defeats the purpose of channels",
    "assertions": [
      {"id": "6.1", "text": "Sends Task values (not *Task pointers) through the channel, OR explicitly documents why pointers are safe in this case"},
      {"id": "6.2", "text": "The channel type is chan Task (value type) rather than chan *Task"},
      {"id": "6.3", "text": "Does not mutate the Task struct after sending it on the channel (or sends a copy)"},
      {"id": "6.4", "text": "If pointers are used, explicitly acknowledges the shared-memory risk and explains the mitigation"}
    ]
  },
  {
    "id": 7,
    "name": "errgroup-vs-waitgroup-decision",
    "description": "Tests whether the model chooses errgroup over WaitGroup when error handling is needed, and uses SetLimit for bounded concurrency",
    "prompt": "Write a Go function that fetches data from 50 URLs concurrently. At most 10 should run at once. If any fetch fails, stop all remaining work and return the error.",
    "trap": "Without the skill, the model builds a hand-rolled worker pool with WaitGroup + semaphore channel, missing errgroup.SetLimit which handles this natively",
    "assertions": [
      {"id": "7.1", "text": "Uses errgroup (golang.org/x/sync/errgroup) instead of sync.WaitGroup for error propagation"},
      {"id": "7.2", "text": "Uses errgroup.WithContext to cancel siblings on first error"},
      {"id": "7.3", "text": "Uses g.SetLimit(10) for bounded concurrency instead of a hand-rolled semaphore"},
      {"id": "7.4", "text": "Does NOT build a manual worker pool with channels and WaitGroup when errgroup suffices"},
      {"id": "7.5", "text": "Each goroutine checks ctx.Done() or uses the context from errgroup.WithContext"}
    ]
  },
  {
    "id": 8,
    "name": "ctx-done-in-select",
    "description": "Tests whether every select statement includes a ctx.Done() case to prevent goroutine leaks",
    "prompt": "Write a Go function that reads from an input channel, transforms each item, and writes to an output channel. It should run as a goroutine.",
    "trap": "Without the skill, the model writes a select with only channel operations but no ctx.Done() case, causing goroutine leaks on cancellation",
    "assertions": [
      {"id": "8.1", "text": "The function accepts a context.Context parameter"},
      {"id": "8.2", "text": "Every select statement includes a case <-ctx.Done(): return branch"},
      {"id": "8.3", "text": "Both the read from input channel AND the write to output channel are wrapped in select with ctx.Done()"},
      {"id": "8.4", "text": "The goroutine exits cleanly when context is cancelled"},
      {"id": "8.5", "text": "The output channel is closed when the goroutine exits (defer close)"}
    ]
  },
  {
    "id": 9,
    "name": "sync-map-vs-rwmutex-decision",
    "description": "Tests whether the model correctly chooses between sync.Map and RWMutex+map based on access patterns",
    "prompt": "I need a concurrent cache in Go. Keys are strings, values are structs. The cache will have frequent writes from many goroutines updating the same keys, and reads happen about as often as writes. Which synchronization approach should I use?",
    "trap": "Without the skill, the model recommends sync.Map because it sounds like 'concurrent map', but sync.Map is slower for write-heavy overlapping-key patterns",
    "assertions": [
      {"id": "9.1", "text": "Recommends sync.RWMutex + plain map over sync.Map for this write-heavy, overlapping-key pattern"},
      {"id": "9.2", "text": "Explains that sync.Map is optimized for write-once/read-many or disjoint key sets"},
      {"id": "9.3", "text": "Explains that for frequent writes with overlapping keys, RWMutex+map is faster"},
      {"id": "9.4", "text": "Does NOT unconditionally recommend sync.Map for any concurrent map scenario"},
      {"id": "9.5", "text": "Mentions that concurrent map read/write without synchronization causes a hard crash (not just a data race)"}
    ]
  },
  {
    "id": 10,
    "name": "sync-pool-reset-before-put",
    "description": "Tests whether the model resets objects before returning them to sync.Pool",
    "prompt": "Write a Go function that uses sync.Pool to reuse bytes.Buffer objects for encoding JSON responses in an HTTP handler. Show the pool setup and the handler.",
    "trap": "Without the skill, the model returns the buffer to the pool without calling Reset(), leading to dirty objects and data corruption",
    "assertions": [
      {"id": "10.1", "text": "Calls buf.Reset() BEFORE bufPool.Put(buf), not after Get()"},
      {"id": "10.2", "text": "Uses defer to ensure the buffer is returned to the pool even on error"},
      {"id": "10.3", "text": "Does not assume the object from Get() is clean/zeroed"},
      {"id": "10.4", "text": "The pool's New function creates a new buffer"},
      {"id": "10.5", "text": "Does not store persistent state in pooled objects"}
    ]
  },
  {
    "id": 11,
    "name": "goroutine-panic-recovery",
    "description": "Tests whether the model adds panic recovery at goroutine boundaries in production code",
    "prompt": "Write a Go HTTP server that spawns a background goroutine per request to do async processing (e.g., sending a notification email). The main handler returns 202 Accepted immediately.",
    "trap": "Without the skill, the model spawns go func() without recover(), so a panic in the background goroutine crashes the entire server process",
    "assertions": [
      {"id": "11.1", "text": "Adds defer func() { recover() }() or equivalent panic recovery inside the goroutine"},
      {"id": "11.2", "text": "Logs or handles the recovered panic (not just silently swallowed)"},
      {"id": "11.3", "text": "The goroutine has a shutdown mechanism (context, done channel, or similar)"},
      {"id": "11.4", "text": "Mentions that a panic in a goroutine crashes the entire process"}
    ]
  },
  {
    "id": 12,
    "name": "singleflight-cache-stampede",
    "description": "Tests whether the model uses singleflight to deduplicate concurrent requests for the same resource",
    "prompt": "My Go service has an expensive database query for user profiles. Under load, hundreds of goroutines request the same user profile simultaneously, causing a thundering herd on the database. How should I solve this?",
    "trap": "Without the skill, the model suggests only traditional caching (TTL-based) or a mutex, missing singleflight which deduplicates in-flight requests",
    "assertions": [
      {"id": "12.1", "text": "Recommends golang.org/x/sync/singleflight as the primary solution"},
      {"id": "12.2", "text": "Shows usage of group.Do(key, func) where key identifies the deduplicated resource"},
      {"id": "12.3", "text": "Explains that only one goroutine executes the function; others wait and share the result"},
      {"id": "12.4", "text": "May combine singleflight with a cache layer for TTL-based caching, but singleflight is the deduplication mechanism"},
      {"id": "12.5", "text": "Does NOT suggest only a plain mutex or only a TTL cache as the solution to thundering herd"}
    ]
  },
  {
    "id": 13,
    "name": "iterator-vs-goroutine-pipeline",
    "description": "Tests whether the model uses Go 1.23+ iterators for sequential CPU-bound transforms instead of goroutine+channel pipelines",
    "prompt": "I have a Go 1.23+ project. I need to filter a slice of users to find active ones, then extract their email addresses. The slice is in-memory and processing is pure CPU. Should I use a goroutine pipeline with channels?",
    "trap": "Without the skill, the model builds a goroutine+channel pipeline for a purely sequential, CPU-bound in-memory transformation where iterators are more appropriate",
    "assertions": [
      {"id": "13.1", "text": "Recommends against goroutine+channel pipeline for this purely sequential, in-memory transform"},
      {"id": "13.2", "text": "Suggests Go 1.23+ iterators (iter.Seq) or simple slice operations instead"},
      {"id": "13.3", "text": "Explains that goroutine+channel pipelines add overhead without benefit for sequential CPU-bound work"},
      {"id": "13.4", "text": "Mentions that goroutine pipelines are appropriate when stages involve I/O or need true parallelism"},
      {"id": "13.5", "text": "Does NOT build a multi-goroutine pipeline for this use case"}
    ]
  }
]
references/channels-and-select.md
# Channels and Select Patterns

## Goroutine Lifecycle

NEVER start a goroutine without knowing how it stops. Every goroutine MUST answer: **how will it stop?**

```go
// ✗ Bad — fire-and-forget, no way to stop or wait
func startWorker() {
    go func() {
        for {
            doWork() // runs forever, leaks on shutdown
        }
    }()
}

// ✓ Good — goroutine respects context cancellation, caller can wait
func startWorker(ctx context.Context) *sync.WaitGroup {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            select {
            case <-ctx.Done():
                return
            default:
                doWork(ctx)
            }
        }
    }()
    return &wg
}
```

### Panic Recovery at Goroutine Boundaries

A panic in a goroutine crashes the entire process. Always recover at goroutine boundaries in production code:

```go
go func() {
    defer func() {
        if r := recover(); r != nil {
            // ...
        }
    }()
    doWork(ctx)
}()
```

## Channel Direction

Specify direction in function signatures to prevent misuse at compile time:

```go
// ✗ Bad — caller could accidentally close or send on a receive-only channel
func consume(ch chan int) { ... }

// ✓ Good — compiler enforces correct usage
func produce(ch chan<- int) { ... } // send-only
func consume(ch <-chan int) { ... } // receive-only
```

## Channel Closing

Channels MUST be closed by the sender (producer), NEVER by the receiver — it causes a panic if the sender writes after close.

```go
// ✓ Good — producer closes when done
func generate(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch) // sender closes
        for i := 0; ; i++ {
            select {
            case ch <- i:
            case <-ctx.Done():
                return
            }
        }
    }()
    return ch
}
```

## Buffer Size

| Size | When to use |
| --- | --- |
| 0 (unbuffered) | Default. Synchronizes sender and receiver — use when you need handoff guarantees |
| 1 | Signal channels (`done := make(chan struct{}, 1)`), or when sender must not block on a single pending item |
| N > 1 | Only with measured justification — document why N was chosen and what happens when the buffer fills |

```go
// ✓ Good — unbuffered for synchronous handoff
ch := make(chan Result)

// ✓ Good — buffered 1 for signal
done := make(chan struct{}, 1)

// ✗ Suspicious — arbitrary large buffer hides backpressure problems
// Give explanation in comments.
ch := make(chan Task, 1000) // why 1000? what if it fills?
```

## Select for Non-Blocking Communication

Use `select` to multiplex channel operations and always include `ctx.Done()` to prevent goroutine leaks:

```go
func process(ctx context.Context, in <-chan Task, out chan<- Result) {
    for {
        select {
        case <-ctx.Done():
            return
        case task, ok := <-in:
            if !ok {
                return // channel closed
            }
            result := handle(ctx, task)
            select {
            case out <- result:
            case <-ctx.Done():
                return
            }
        }
    }
}
```

## Avoid Repeated `time.After` in Hot Loops

```go
// ✗ Bad — creates a new timer on every iteration
for {
    select {
    case msg := <-ch:
        handle(msg)
    case <-time.After(5 * time.Second): // repeated allocation/churn
        handleTimeout()
    }
}

// ✓ Good (Go 1.23+) — reuse the timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
    select {
    case msg := <-ch:
        timer.Stop()
        timer.Reset(5 * time.Second)
        handle(msg)
    case <-timer.C:
        handleTimeout()
        timer.Reset(5 * time.Second)
    }
}
```

For Go <1.23, if `timer.Stop()` returns false, drain a possible stale value before `Reset`. In Go 1.23+, receiving from `timer.C` after `Stop` returns is guaranteed to block rather than receive a stale value.
references/pipelines.md
# Pipelines and Worker Pools

## Pipeline Pattern

A pipeline is a series of stages connected by channels, where each stage is a goroutine (or group of goroutines) that:

1. Receives values from an upstream channel
2. Processes each value
3. Sends results to a downstream channel

```go
// Stage 1: Generate integers
func generate(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

// Stage 2: Square each integer
func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

// Usage
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    ch := generate(ctx, 2, 3, 4)
    results := square(ctx, ch)

    for v := range results {
        fmt.Println(v) // 4, 9, 16
    }
}
```

**Key rules for pipelines**:

- Pipeline stages MUST accept and respect context cancellation — every stage must select on `ctx.Done()` to avoid goroutine leaks on early cancellation
- The producer (first stage) closes its output channel; each subsequent stage closes its own output
- NEVER create unbounded goroutines in pipeline stages
- Use unbuffered channels unless you have measured throughput needs

## Fan-Out / Fan-In

**Fan-out**: multiple goroutines read from the same channel to parallelize CPU-bound work. **Fan-in**: multiple channels are merged into a single output channel.

```go
// Fan-out: N workers reading from the same input channel
func fanOut(ctx context.Context, in <-chan Task, workers int) <-chan Result {
    out := make(chan Result)
    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for {
                select {
                case task, ok := <-in:
                    if !ok {
                        return
                    }
                    select {
                    case out <- process(ctx, task):
                    case <-ctx.Done():
                        return
                    }
                case <-ctx.Done():
                    return
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}
```

```go
// Fan-in: merge multiple channels into one
func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result {
    out := make(chan Result)
    var wg sync.WaitGroup

    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan Result) {
            defer wg.Done()
            for v := range c {
                select {
                case out <- v:
                case <-ctx.Done():
                    return
                }
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}
```

## Worker Pool with errgroup

Fan-out workers SHOULD use `errgroup.SetLimit` for bounded concurrency. For most use cases, `errgroup.SetLimit` replaces hand-rolled worker pools:

```go
func processAll(ctx context.Context, tasks []Task) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // max 10 concurrent workers

    for _, task := range tasks {
        g.Go(func() error {
            return process(ctx, task)
        })
    }
    return g.Wait()
}
```

Use a hand-rolled worker pool only when you need:

- Per-worker state (connections, buffers)
- Custom backpressure or priority scheduling
- Graceful draining with in-flight task completion

## Bounded Concurrency with Semaphore

When you need fine-grained concurrency control without errgroup:

```go
func processAll(ctx context.Context, items []Item) error {
    sem := make(chan struct{}, 10) // semaphore of 10
    var wg sync.WaitGroup

    for _, item := range items {
        wg.Add(1)
        sem <- struct{}{} // acquire
        go func(item Item) {
            defer wg.Done()
            defer func() { <-sem }() // release
            process(ctx, item)
        }(item)
    }
    wg.Wait()
    return nil
}
```

Prefer `errgroup.SetLimit` over this pattern when error propagation is needed.

## Pipeline Alternatives

### Go 1.23+ Iterators (range-over-func)

For in-process data transformations that do not need concurrency, iterators avoid the overhead of goroutines and channels:

```go
func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if pred(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for v := range seq {
            if !yield(f(v)) {
                return
            }
        }
    }
}
```

Use iterators when:

- Processing is CPU-bound and does not benefit from parallelism
- You want lazy evaluation without goroutine overhead
- The data source is already sequential (slice, database cursor)

Use goroutine+channel pipelines when:

- Stages involve I/O (network, disk) that benefits from concurrency
- You need true parallelism across CPU cores
- Stages have different throughput characteristics

### samber/ro

`samber/ro` provides a fluent, type-safe pipeline API for read-only collections:

```go
import "github.com/samber/ro"

emails, _ := ro.Collect( // ignore error
    ro.Pipe(
        ro.FromSlice(users),
        ro.Filter(func(u User) bool { return u.Active }),
        ro.Map(func(u User) string { return u.Email }),
    ),
)

```

Use `samber/ro` for sequential data transformations that benefit from a fluent API. It might also support parallel processing if needed.

## Goroutine Leak Detection

Goroutine leaks SHOULD be detected with goleak in tests. Use `go.uber.org/goleak` in `TestMain` to catch leaked goroutines across all tests:

```go
func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}
```

## Common Pipeline Mistakes

| Mistake | Fix |
| --- | --- |
| Missing `ctx.Done()` in pipeline stage | Always select on context to allow cancellation |
| Not closing output channel | Producer must `defer close(out)` |
| Unbounded goroutine spawning | Use `errgroup.SetLimit` or a semaphore |
| Sending mutable data through channel | Send copies or immutable values |
| Blocking send without select | Wrap channel sends in select with `ctx.Done()` |

→ See `samber/cc-skills-golang@golang-concurrency` skill for sync primitives and channel patterns.
references/sync-primitives.md
# Sync Primitives Deep Dive

## sync.Mutex

Protects shared state with exclusive access. MUST hold the lock for the shortest time possible — NEVER hold a mutex across I/O, network calls, or channel operations.

```go
type SafeCache struct {
    mu    sync.Mutex
    items map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    v, ok := c.items[key]
    return v, ok
}

func (c *SafeCache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = value
}
```

### Embedding Convention

Embed the mutex as an unexported field, placed directly above the fields it protects:

```go
type Registry struct {
    mu      sync.Mutex // protects entries
    entries map[string]Entry
}
```

## sync.RWMutex

SHOULD be used when reads greatly outnumber writes. Multiple goroutines can hold `RLock` simultaneously; `Lock` is exclusive.

```go
type Config struct {
    mu     sync.RWMutex
    values map[string]string
}

func (c *Config) Get(key string) string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.values[key]
}

func (c *Config) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.values[key] = value
}
```

**Pitfall**: Do not upgrade RLock to Lock — this deadlocks. Release RLock first, then acquire Lock.

## sync/atomic

Lock-free operations for simple values. SHOULD be preferred over Mutex for simple counter operations. Faster than mutex for low-contention counters and flags.

```go
// ✓ Good — atomic for a simple counter
var requestCount atomic.Int64

func handleRequest() {
    requestCount.Add(1)
}

func getCount() int64 {
    return requestCount.Load()
}
```

```go
// ✓ Good — atomic.Bool for a shutdown flag
var shuttingDown atomic.Bool

func shutdown() {
    shuttingDown.Store(true)
}

func isRunning() bool {
    return !shuttingDown.Load()
}
```

Go 1.19+ provides typed atomics (`atomic.Int64`, `atomic.Bool`, `atomic.Pointer[T]`) — prefer these over raw `atomic.AddInt64`/`atomic.LoadInt64`.

## sync.Map

SHOULD only be used for write-once/read-many patterns. Optimized for two common patterns: (1) keys are written once and read many times, (2) multiple goroutines read/write disjoint key sets. For other patterns, a plain `map` + `sync.RWMutex` is faster.

```go
var cache sync.Map

func Get(key string) (any, bool) {
    return cache.Load(key)
}

func Set(key string, value any) {
    cache.Store(key, value)
}

func GetOrSet(key string, compute func() any) any {
    if v, ok := cache.Load(key); ok {
        return v
    }
    v, _ := cache.LoadOrStore(key, compute())
    return v
}
```

**When NOT to use `sync.Map`**: when you need to iterate, get the length, or when writes are frequent and keys overlap heavily. Use `sync.RWMutex` + `map` instead.

## sync.Pool

Reuse temporary objects to reduce GC pressure. MUST NOT store pointers to stack-allocated objects. Objects in the pool may be reclaimed at any GC cycle — do not store persistent state.

```go
var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}

func process(data []byte) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()

    buf.Write(data)
    // ... transform ...
    return buf.String()
}
```

**Rules**:

- Always `Reset()` before `Put()` — returning dirty objects causes bugs
- Do not assume an object from `Get()` is zeroed — the `New` func only runs if the pool is empty
- Best for short-lived, frequently allocated objects (buffers, encoders, temporary structs)

## sync.Once

MUST be used for one-time initialization. Execute exactly once, regardless of how many goroutines call it concurrently. Thread-safe by design.

```go
type DBClient struct {
    initOnce  sync.Once
    closeOnce sync.Once
    conn      *sql.DB
}

func (c *DBClient) getConn() *sql.DB {
    c.initOnce.Do(func() {
        var err error
        c.conn, err = sql.Open("postgres", dsn)
        if err != nil {
            panic(fmt.Sprintf("db init: %v", err))
        }
    })
    return c.conn
}

func (c *DBClient) Close() error {
    var err error
    c.closeOnce.Do(func() {
        err = c.conn.Close()
    })
    return err
}
```

Go 1.21+ also provides `sync.OnceFunc`, `sync.OnceValue`, and `sync.OnceValues` for simpler use cases:

```go
var loadConfig = sync.OnceValue(func() *Config {
    cfg, err := parseConfig("config.yaml")
    if err != nil {
        panic(err)
    }
    return cfg
})

// Usage: cfg := loadConfig()
```

## sync.WaitGroup

Use `sync.WaitGroup` when you only need to wait for a set of goroutines to finish.

### Go 1.25+: `wg.Go`

`WaitGroup.Go` starts a goroutine, adds it to the group, and removes it from the group when the function returns.

```go
func processAll(items []Item) {
    var wg sync.WaitGroup

    for _, item := range items {
        // Go 1.22+ loop variables are per-iteration when the module has `go 1.22+`.
        // Do not add `item := item` solely for closure capture in modern modules.
        wg.Go(func() {
            process(item)
        })
    }

    wg.Wait()
}
```

Rules:

- `WaitGroup.Go` is Go 1.25+, not Go 1.24.
- The function passed to `wg.Go` must not panic.
- `WaitGroup` does not propagate errors and does not cancel siblings.
- For first-error-wins, cancellation, concurrency limits, or returned values, use `golang.org/x/sync/errgroup`.

**Benefits of `wg.Go()`**:

- No manual `Add`/`Done` bookkeeping
- Lower risk of `Add`/`Wait` ordering bugs
- Cleaner API for simple fire-and-wait work

**When to use**: Go 1.25+ projects for simple goroutines that must all finish, do not return errors, do not need cancellation, and must not panic. Use `errgroup` when work returns errors, needs cancellation, limits, or first-error behavior.

### Go <1.25 fallback

```go
func processAll(ctx context.Context, items []Item) {
    var wg sync.WaitGroup
    for _, item := range items {
        wg.Add(1) // Add BEFORE go
        go func(item Item) {
            defer wg.Done()
            process(ctx, item)
        }(item)
    }
    wg.Wait() // blocks until all goroutines finish
}
```

```go
// ✗ Bad — Add inside the goroutine (race: Wait may return before Add runs)
go func() {
    wg.Add(1)
    defer wg.Done()
    process(item)
}()
```

## golang.org/x/sync/singleflight

Deduplicates concurrent calls for the same key. When multiple goroutines request the same resource simultaneously, only one executes; the rest wait and share the result.

```go
var group singleflight.Group

func GetUser(ctx context.Context, id string) (*User, error) {
    v, err, _ := group.Do(id, func() (any, error) {
        // Only one goroutine executes this for a given id
        return db.QueryUser(ctx, id)
    })
    if err != nil {
        return nil, err
    }
    return v.(*User), nil
}
```

**Use cases**: cache stampede prevention, deduplicating expensive lookups (DB, API), rate-limited external service calls.

## golang.org/x/sync/errgroup

Goroutine group with error propagation. Returns the first error from any goroutine. With `WithContext`, cancels remaining goroutines on first error.

```go
func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
    g, ctx := errgroup.WithContext(ctx) // cancel siblings on first error
    results := make([]Response, len(urls))

    for i, url := range urls {
        g.Go(func() error {
            resp, err := fetch(ctx, url)
            if err != nil {
                return fmt.Errorf("fetching %s: %w", url, err)
            }
            results[i] = resp // safe: each goroutine writes to its own index
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}
```

### Bounded Concurrency with SetLimit

SHOULD use `SetLimit` to bound concurrency and avoid unbounded goroutine spawning.

```go
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // at most 10 goroutines run concurrently

for _, task := range tasks {
    g.Go(func() error {
        return process(ctx, task)
    })
}
return g.Wait()
```

This replaces hand-rolled worker pools for most use cases.

→ See `samber/cc-skills-golang@golang-concurrency` skill for high-level patterns and decision trees.