go-concurrency-review
Skill85 starsChanged 18 days agoMITView on GitHub
Review and implement safe concurrency patterns in Go: goroutines, channels, sync primitives, context propagation, and goroutine lifecycle management. Use when writing concurrent code, reviewing async patterns, checking thread safety, debugging race conditions, or designing producer/consumer pipelines. Trigger examples: "check thread safety", "review goroutines", "race condition", "channel patterns", "sync.Mutex", "context cancellation", "goroutine leak". Not for: general style (go-coding-standards), HTTP handler patterns (go-api-design).
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 35
Read a reference file only when the section below is not enough.
- Very specific: line 63
Context propagation: verify blocking calls accept and respect context.Context.
- Very specific: line 66
If your environment supports delegating work to parallel sub-agents or tasks, assign each pass to one.
- Distinctive: line 90
EVERY goroutine MUST have a clear termination path.
- Distinctive: line 176
Keep the critical section minimal — never call out to an external service, or take a second lock, while holding one.
- Very specific: line 180
Use sync/atomic types for counters and flags rather than a mutex around an int64.
- Distinctive: line 225
ALWAYS run tests with race detector during CI:
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Go Concurrency Review
Concurrency in Go is powerful and deceptively easy to get wrong. These patterns prevent goroutine leaks, data races, and deadlocks.
Detailed reference material, loaded on demand:
references/channels.md— sizing, signalling, producer shutdown.references/mutex-and-atomics.md— mutex placement, lock scope, atomics,sync.Once, RWMutex pitfalls,sync.Map,sync.Pool,singleflight.references/goroutine-patterns.md— panic recovery at goroutine boundaries, pipelines, bounded fan-out, timers.
Read a reference file only when the section below is not enough.
Operating Modes
Pick the mode that matches the request before starting:
- Implementation — writing new concurrent code. Follow the patterns below as construction rules.
- Diff review (default) — check changed code against every section,
paying extra attention to new
gostatements and shared state. - Leak/race hunt — a symptom is already observed (growing goroutine
count,
-racereport, deadlock). Start from "Auditing Large Codebases" and the Race Detection section to localize it. On Go 1.27+ read thegoroutineleakprofile first: it lists goroutines blocked on something nothing can ever unblock. The go-troubleshooting skill owns the procedure.
Auditing Large Codebases
For a full concurrency audit, run these independent passes rather than one linear read:
- Goroutine lifecycle: find every
gostatement (grep -rn "go func\|go [a-zA-Z]" --include="*.go") and verify each has a termination path (context, closed channel, WaitGroup). - Shared state: find package-level vars and struct fields accessed from multiple goroutines; verify mutex/atomic protection.
- Channel topology: map producers/consumers per channel; verify close-exactly-once and no send-on-closed paths.
- Context propagation: verify blocking calls accept and respect
context.Context.
If your environment supports delegating work to parallel sub-agents or
tasks, assign each pass to one; otherwise run them in order. Findings
must cite file.go:line. Always finish with go test -race ./....
Choosing a Primitive
| Need | Use | Why |
| Hand data from one goroutine to another | Channel | The send transfers ownership |
| Guard fields of a shared struct | sync.Mutex / RWMutex | Short critical sections, explicit owner |
| A counter or flag | atomic.Int64, atomic.Bool | No lock, no torn reads |
| Compute once, read many | sync.OnceValue | Exactly once, even under contention |
| Collapse concurrent identical calls | singleflight.Group | One backend call per key, not N |
| Reuse short-lived buffers on a hot path | sync.Pool | Fewer allocations — only after a profile |
| Waiting for goroutines | Use |
| Results don't fail | sync.WaitGroup (wg.Go, Go 1.25+) |
| First error matters | errgroup.Group |
| First error cancels the rest | errgroup.WithContext |
| Cap how many run at once | errgroup + SetLimit(n) — not a hand-rolled pool |
1. Goroutine Lifecycle Management
EVERY goroutine MUST have a clear termination path. No fire-and-forget.
Use errgroup for coordinated goroutines:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch data: %w", err)
}Long-running goroutines must respect context:
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
if err := w.process(job); err != nil {
w.logger.Error("process job", slog.Any("error", err))
}
}
}
}Start goroutines in the owner, not the callee:
// ✅ Good — caller controls lifecycle
go worker.Run(ctx)
// ❌ Bad — function secretly starts goroutine
func NewWorker() *Worker {
w := &Worker{}
go w.run() // hidden goroutine — caller has no control
return w
}Recover at the goroutine boundary
A panic in any goroutine terminates the whole process. The recover in
HTTP middleware covers the request goroutine only, not goroutines it
starts. Workers and background tasks that run input-dependent or
third-party code recover in their own deferred function, log the stack,
and turn the panic into an error. Don't recover to hide bugs in code you
own — recover where a crash would take unrelated work down with it.
Example: references/goroutine-patterns.md.
2. Channel Patterns
- Size is one or none. Unbuffered is a synchronization point; buffer 1 is a
handoff. Any larger buffer needs a comment justifying the number — an
arbitrary
100is a bug waiting for the day production is slower than staging. - Signal channels carry
struct{}, andclose(done)broadcasts to every receiver at once. - The producer owns the channel and is the only one that closes it, with
defer close(ch)in the producing goroutine. Every send sits in aselectagainstctx.Done(), or a consumer that walks away leaks the producer. - Pipelines: each stage closes its own output; fan-out is bounded with
errgroup.SetLimit; a single-goroutine chain is aniter.Seq, not channels.
Examples in references/channels.md and references/goroutine-patterns.md.
3. Mutexes and Atomics
- Zero-value
sync.Mutexandsync.RWMutexare ready to use. A*sync.Mutexfield is always wrong. - Declare the mutex directly above the fields it guards, with a comment naming the relationship. A mutex that guards "the struct" guards nothing in particular.
- Keep the critical section minimal — never call out to an external service, or take a second lock, while holding one.
- 🔴 Never copy a value containing a mutex (
c2 := *c1): the copy carries the original's lock state.go vetcatches most of these; trust it. - Use
sync/atomictypes for counters and flags rather than a mutex around anint64. sync.Oncefor lazy initialization that must happen exactly once.RWMutexcannot be upgraded or read-locked recursively;sync.Mapfits two workloads only;sync.Poolobjects must be reset and size-capped;singleflightruns on the first caller's context — detach it. Details inreferences/mutex-and-atomics.md.
Examples in references/mutex-and-atomics.md.
4. Context Propagation
Context is always the first parameter, never a struct field, and every
blocking operation selects on ctx.Done():
// ✅ Good
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ Bad — blocks forever if the context is cancelled
result := <-chDerive a child context with its own timeout for each external call and
defer cancel() immediately. Full rules in the go-context skill.
5. Avoid Mutable Globals
// ❌ Bad — mutable global, not safe for concurrent access
var db *sql.DB
// ✅ Good — pass as dependency
type Server struct {
db *sql.DB
}Race Detection
ALWAYS run tests with race detector during CI:
go test -race ./...This is non-negotiable. A test suite that passes without -race proves nothing
about concurrent correctness.
Red Flags Checklist
- 🔴 Goroutine started without shutdown path
- 🔴 Channel never closed (potential goroutine leak)
- 🔴 Mutex copied by value
- 🔴 Context stored in struct field
- 🔴
context.Background()used where parent context was available - 🔴
selectwithoutctx.Done()case in blocking operation - 🔴 Shared map/slice accessed without synchronization
- 🟡 Buffered channel with arbitrary large size
- 🟡
time.Sleepused for synchronization instead of proper signaling - 🟡 Goroutine starting inside
init()or constructor without lifecycle control - 🟡 Background goroutine running input-dependent code with no
recover
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- johnqtcg/awesome-skills★ 30same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository38 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- .cursor/rules/go-skills.mdcCursor rule
- go-api-designSkill
- go-architecture-reviewSkill
- go-cliSkill
- go-dependency-injectionSkill
- go-design-patternsSkill
- go-graphqlSkill
- go-grpcSkill
- go-interface-designSkill
- go-openapiSkill
- go-project-layoutSkill
- go-code-reviewSkill
- go-coding-standardsSkill
- go-contextSkill
- go-data-structuresSkill
- go-documentationSkill
- go-error-handlingSkill
- go-modernizeSkill
- go-namingSkill
- go-databaseSkill
- go-defensive-codingSkill
- go-observabilitySkill
- go-performance-reviewSkill
- go-security-auditSkill
- go-troubleshootingSkill
- go-test-qualitySkill
- go-test-table-drivenSkill
- git-commitSkill
- go-binary-sizeSkill
- go-build-releaseSkill
- go-ciSkill
- go-dependency-auditSkill
- go-refactoringSkill
- go-semantic-toolsSkill
- go-skills-routerSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

