go-concurrency

$npx mdskill add cxuu/golang-skills/go-concurrency

Ensure goroutines are properly managed and terminated in Go code

  • Prevent goroutine leaks by ensuring they have a clear exit strategy.
  • Use channels for signaling termination to goroutines and managing their lifetimes.
  • Decide on the appropriate synchronization primitives (e.g., mutexes, atomics) based on the need for mutual exclusion or shared state management.
  • Deliver results through proper error handling and resource cleanup mechanisms.

SKILL.md

.github/skills/go-concurrencyView on GitHub ↗
---
name: go-concurrency
description: Use when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
---

# Go Concurrency

> Compatibility: Atomic examples may use standard-library typed atomics where available or `go.uber.org/atomic` where a project already depends on it.

## Resource Routing

- `references/GOROUTINE-PATTERNS.md` - Read when starting, stopping, or waiting for goroutines.
- `references/SYNC-PRIMITIVES.md` - Read when choosing between mutexes, atomics, channels, and once-like primitives.
- `references/BUFFER-POOLING.md` - Read when considering channel-backed or sync.Pool-style reuse.
- `references/ADVANCED-PATTERNS.md` - Read for worker pools, pipelines, errgroup, and cancellation-heavy patterns.

## Goroutine Lifetimes

> **Normative**: When you spawn goroutines, make it clear when or whether they
> exit.

Goroutines can leak by blocking on channel sends/receives. The GC **will not
terminate** a blocked goroutine even if no other goroutine holds a reference to
the channel. Even non-leaking in-flight goroutines cause panics (send on closed
channel), data races, memory issues, and resource leaks.

### Core Rules

1. **Every goroutine needs a stop mechanism** — a predictable end time, a
   cancellation signal, or both
2. **Code must be able to wait** for the goroutine to finish
3. **No goroutines in `init()`** — expose lifecycle methods (`Close`, `Stop`,
   `Shutdown`) instead
4. **Keep synchronization scoped** — constrain to function scope, factor logic
   into synchronous functions

```go
// Good: Clear lifetime with WaitGroup.Go (Go 1.25+)
var wg sync.WaitGroup
for item := range queue {
    item := item
    wg.Go(func() { process(ctx, item) })
}
wg.Wait()
```

```go
// Bad: No way to stop or wait
go func() { for { flush(); time.Sleep(delay) } }()
```

**Test for leaks** with [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak).

> **Principle**: Never start a goroutine without knowing how it will stop.

---

## Share by Communicating

> "Do not communicate by sharing memory; instead, share memory by communicating."

This is Go's foundational concurrency design principle. Use **channels** for
ownership transfer and orchestration — when one goroutine produces a value and
another consumes it. Use **mutexes** when multiple goroutines access shared
state and channels would add unnecessary complexity.

**Default to channels.** Fall back to `sync.Mutex` / `sync.RWMutex` when the
problem is naturally about protecting a shared data structure (e.g., a cache or
counter) rather than passing data between goroutines.

---

## Synchronous Functions

> **Normative**: Prefer synchronous functions over asynchronous ones.

| Benefit | Why |
|---|---|
| Localized goroutines | Lifetimes easier to reason about |
| Avoids leaks and races | Easier to prevent resource leaks and data races |
| Easier to test | Check input/output without polling |
| Caller flexibility | Caller adds concurrency when needed |

> **Advisory**: It is quite difficult (sometimes impossible) to remove
> unnecessary concurrency at the caller side. Let the caller add concurrency
> when needed.

---

## Zero-value Mutexes

The zero-value of `sync.Mutex` and `sync.RWMutex` is valid — almost never need
a pointer to a mutex.

```go
// Good: Zero-value is valid    // Bad: Unnecessary pointer
var mu sync.Mutex                mu := new(sync.Mutex)
```

**Don't embed mutexes** — use a named `mu` field to keep `Lock`/`Unlock` as
implementation details, not exported API.

---

## Channel Direction

> **Normative**: Specify channel direction where possible.

Direction prevents errors (compiler catches closing a receive-only channel),
conveys ownership, and is self-documenting.

```go
func produce(out chan<- int) { /* send-only */ }
func consume(in <-chan int)  { /* receive-only */ }
func transform(in <-chan int, out chan<- int) { /* both */ }
```

### Channel Size: One or None

Channels should have size **zero** (unbuffered) or **one**. Any other size
requires justification for:

- How the size was determined
- What prevents the channel from filling under load
- What happens when writers block

```go
c := make(chan int)    // unbuffered — Good
c := make(chan int, 1) // size one — Good
c := make(chan int, 64) // arbitrary — needs justification
```

---

## Atomic Operations

Use `atomic.Bool`, `atomic.Int64`, etc. (stdlib `sync/atomic` since Go 1.19, or
[go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic)) for type-safe
atomic operations. Raw `int32`/`int64` fields make it easy to forget atomic
access on some code paths.

```go
// Good: Type-safe              // Bad: Easy to forget
var running atomic.Bool          var running int32 // atomic
running.Store(true)              atomic.StoreInt32(&running, 1)
running.Load()                   running == 1 // race!
```

---

## Documenting Concurrency

> **Advisory**: Document thread-safety when it's not obvious from the operation
> type.

Go users assume read-only operations are safe for concurrent use, and mutating
operations are not. Document concurrency when:

1. **Read vs mutating is unclear** — e.g., a `Lookup` that mutates LRU state
2. **API provides synchronization** — e.g., thread-safe clients
3. **Interface has concurrency requirements** — document in type definition

---

## Context Usage

> For context.Context guidance (parameter placement, struct storage, custom
> types, derivation patterns), see the dedicated
> [go-context](../go-context/SKILL.md) skill.

---

## Buffer Pooling with Channels

Use a buffered channel as a free list to reuse allocated buffers. This "leaky
buffer" pattern uses `select` with `default` for non-blocking operations.

---

## Related Skills

- **Context propagation**: See [go-context](../go-context/SKILL.md) when passing cancellation, deadlines, or request-scoped values through goroutines
- **Error handling**: See [go-error-handling](../go-error-handling/SKILL.md) when propagating errors from goroutines or using errgroup
- **Defensive hardening**: See [go-defensive](../go-defensive/SKILL.md) when protecting shared state at API boundaries or using defer for cleanup
- **Interface design**: See [go-interfaces](../go-interfaces/SKILL.md) when choosing receiver types for types with sync primitives

### External Resources

- [Never start a goroutine without knowing how it will
  stop](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop)
  — Dave Cheney
- [Rethinking Classical Concurrency
  Patterns](https://www.youtube.com/watch?v=5zXAHh5tJqQ) — Bryan Mills
  (GopherCon 2018)
- [When Go programs end](https://changelog.com/gotime/165) — Go Time podcast
- [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) — Goroutine leak
  detector for testing
- [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) — Type-safe
  atomic operations

More from cxuu/golang-skills

SkillDescription
go-code-reviewUse when reviewing Go code or checking code against community style standards. Also use proactively before submitting a Go PR or when reviewing any Go code changes, even if the user doesn't explicitly request a style review. Does not cover language-specific syntax — delegates to specialized skills.
go-contextUse when working with context.Context in Go — placement in signatures, propagating cancellation and deadlines, and storing values in context vs parameters. Also use when cancelling long-running operations, setting timeouts, or passing request-scoped data, even if they don't mention context.Context directly. Does not cover goroutine lifecycle or sync primitives (see go-concurrency).
go-control-flowUse when writing conditionals, loops, or switch statements in Go — including if with initialization, early returns, for loop forms, range, switch, type switches, and blank identifier patterns. Also use when writing a simple if/else or for loop, even if the user doesn't mention guard clauses or variable scoping. Does not cover error flow patterns (see go-error-handling).
go-data-structuresUse when working with Go slices, maps, or arrays — choosing between new and make, using append, declaring empty slices (nil vs literal for JSON), implementing sets with maps, and copying data at boundaries. Also use when building or manipulating collections, even if the user doesn't ask about allocation idioms. Does not cover concurrent data structure safety (see go-concurrency).
go-declarationsUse when declaring or initializing Go variables, constants, structs, or maps — including var vs :=, reducing scope with if-init, formatting composite literals, designing iota enums, and using any instead of interface{}. Also use when writing a new struct or const block, even if the user doesn't ask about declaration style. Does not cover naming conventions (see go-naming).
go-defensiveUse when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).
go-documentationUse when writing or reviewing documentation for Go packages, types, functions, or methods. Also use proactively when creating new exported types, functions, or packages, even if the user doesn't explicitly ask about documentation. Does not cover code comments for non-exported symbols (see go-style-core).
go-error-handlingUse when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).
go-functional-optionsUse when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).
go-functionsUse when organizing functions within a Go file, formatting function signatures, designing return values, or following Printf-style naming conventions. Also use when a user is adding or refactoring any Go function, even if they don't mention function design or signature formatting. Does not cover functional options constructors (see go-functional-options).