Skip to content
Back to skills

Golang Concurrency

ASecurity

Choose and apply the right Go concurrency primitive — channels, mutexes, atomics, copy-on-write, singleflight, and lock-free data structures. Use when designing concurrent access to shared state, diagnosing lock contention (paired with golang-profiling), choosing between sync.RWMutex and atomic.Pointer copy-on-write, evaluating concurrent map options (sync.Map vs xsync.MapOf), implementing singleflight request coalescing, or reaching for a lock-free queue/ring-buffer library. Covers stdlib sy...

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
developmentrustgorefactoringgitdatabasebackend

Security analysis

A100/100

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

Scanned October 5, 2026

npx -y skills add tstapler/dotfiles --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/tstapler-golang-concurrency/badge)](https://www.skillsdirectory.com/skills/tstapler-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: Choose and apply the right Go concurrency primitive — channels, mutexes, atomics, copy-on-write, singleflight, and lock-free data structures. Use when designing concurrent access to shared state, diagnosing lock contention (paired with golang-profiling), choosing between sync.RWMutex and atomic.Pointer copy-on-write, evaluating concurrent map options (sync.Map vs xsync.MapOf), implementing singleflight request coalescing, or reaching for a lock-free queue/ring-buffer library. Covers stdlib sync/atomic, golang.org/x/sync, puzpuzpuz/xsync, Workiva/go-datastructures, and golang-design/lockfree.
---

# Go Concurrency

A decision framework for concurrent access to shared state in Go, plus the specific libraries and data structures worth reaching for. Pairs with `golang-development` (general idiom) and `golang-profiling` (proving contention is real before fixing it).

---

## Core Principle

> **Never hold a mutex across I/O.**
> Any lock held during a network call, git subprocess, database query, or file read
> serializes all concurrent callers for the full duration of that I/O. Find it, kill it.

---

## The Ladder

Stop at the first rung that holds. Don't skip to lock-free structures because they sound fast — most contention is fixed at rung 2 or 3.

1. **Do these goroutines need to share state at all?** If one goroutine can own the data and others talk to it via a channel, do that. (Go proverb: share memory by communicating.)
2. **Is it a single value — counter, flag?** Use a typed `sync/atomic` wrapper. No mutex needed.
3. **Is it a struct or pointer read constantly, written rarely?** Use the copy-on-write/RCU pattern — `atomic.Pointer[T]` for a single struct, `atomic.Value` for a multi-field snapshot, or a COW slice for rarely-updated lists. Readers pay one atomic load, zero lock contention.
4. **Is it a small critical section with mixed reads/writes at moderate concurrency?** Plain `sync.Mutex` or `sync.RWMutex`. This is correct for the vast majority of Go code — don't preemptively avoid it.
5. **Is it a concurrent map?** Pick `sync.Map` (stable keys, read-heavy), `xsync.MapOf` (typed, higher write throughput on Go < 1.24), or a sharded map (extreme write-heavy + high key cardinality).
6. **Duplicate concurrent requests for the same expensive operation?** `singleflight.Group` — coalesces N identical in-flight calls into 1.
7. **Is it a genuine high-throughput producer/consumer queue and you've profiled `sync.Mutex`/channels as the bottleneck?** Only now consider a third-party lock-free queue/ring buffer.

Never start at rung 7. Lock-free data structures fix a narrow problem and are easy to misapply.

---

## Decision Tree

```
What kind of shared state?
│
├── Single primitive (int64, bool, uint32)?
│   └── atomic.Int64 / atomic.Bool / atomic.Uint32
│
├── Single pointer to struct, read on every request, written rarely?
│   └── atomic.Pointer[T] (Go 1.19+) — single atomic load, zero lock
│
├── Multiple fields that must be read consistently (config, auth state, snapshot)?
│   └── atomic.Value storing an IMMUTABLE snapshot struct
│       → Readers call Load(); writers build a new struct and call Store()
│       → Never mutate the stored struct in place after Store()
│
├── Rarely-modified list (plugin registry, subscriber list, poller instances)?
│   └── COW slice via atomic.Value + writeMu to serialize writers
│
├── One-time initialization?
│   └── sync.Once — guaranteed single execution, zero lock after first call
│
├── Concurrent map?
│   ├── Write-rarely / read-often (cache, registry, ETag store):
│   │   └── sync.Map — lock-free reads in the steady state
│   ├── Typed values, moderate writes, or Go < 1.24:
│   │   └── xsync.MapOf[K, V] (puzpuzpuz/xsync/v4) — fine-grained sharded locking
│   │       → Use .Compute() for CAS-style atomic updates
│   │       → Structurally impossible to hold its internal lock across I/O
│   └── Extremely write-heavy, high key cardinality:
│       └── Sharded map: N xsync.MapOf shards, key routed by hash(key) % N
│
├── Duplicate concurrent requests for the same expensive operation?
│   └── singleflight.Group — coalesces N identical in-flight calls into 1
│       → Combine with atomic.Value/sync.Map cache: check cache first, miss → singleflight
│
├── Parallel work with first-error cancellation?
│   └── errgroup.WithContext (golang.org/x/sync)
│
├── Limit concurrency to N at a time?
│   └── semaphore (golang.org/x/sync)
│
├── Truly complex multi-field update where atomics don't compose?
│   └── sync.Mutex (last resort)
│       → Critical section must be < 10µs (no I/O, no subprocess, no allocation loop)
│       → Prefer sync.RWMutex if read:write ratio > 5:1
│       → Document WHY a mutex was unavoidable
│
└── Genuine MPMC queue bottleneck, confirmed by profiling?
    └── Workiva/go-datastructures (bounded, blocking, Dispose()) or golang-design/lockfree (unbounded)
```

---

## Rung 2-3: Typed Atomics and Copy-on-Write

**Rung 2 (single primitive)**: use a typed atomic (`atomic.Int64`, `atomic.Bool`, `atomic.Pointer[T]`) instead of raw `unsafe.Pointer` or untyped `atomic.Value`. Watch for the noCopy pitfall — these types embed a `noCopy` sentinel `go vet` flags if the containing struct is copied by value.

**Rung 3 (read-mostly struct or list)**: writers build a full new immutable value and atomically swap the pointer; readers pay one atomic load, zero lock contention. This beats `sync.RWMutex` at high core counts because `RLock()` still does an atomic increment that bounces cache lines across readers. Three shapes: `atomic.Pointer[T]` for a single uniform struct, `atomic.Value` for a multi-field snapshot whose type isn't uniform across stores, and a COW slice (`atomic.Value` + a writer-side `sync.Mutex`) for rarely-updated lists like plugin/subscriber registries.

See [Atomics and Copy-on-Write](references/atomics-and-copy-on-write.md) for the full cheat sheet and code for all three COW shapes.

---

## Rung 4: sync.Mutex / sync.RWMutex

Still correct for most code. A `sync.Mutex` is the right choice when ALL of these are true:

1. The critical section contains no I/O, no subprocess, no allocation-heavy loop.
2. The critical section runs in < 10µs in the worst case.
3. The write:read ratio is high enough that `sync.RWMutex` would add complexity without benefit.
4. `singleflight` cannot coalesce the callers (different keys, non-idempotent operations).

**Rules that actually matter**: never hold a lock across I/O; `RWMutex` only pays off when reads significantly outnumber writes on a non-trivial critical section (plain `Mutex` can win on short sections); keep critical sections to field mutation only, watching for lock-order cycles; shard the mutex once one struct serves many independent keys.

See [Mutexes and Concurrent Maps](references/mutexes-and-concurrent-maps.md) for the full rules and code.

---

## Rung 5: Concurrent Maps

Pick `sync.Map` for stable keys with read-heavy access (0 B/op at 99% reads — benchmark your workload; Go 1.24's HashTrieMap backend narrows the gap further). Pick `xsync.MapOf[K, V]` (puzpuzpuz/xsync/v4) for typed generics, higher write throughput on Go < 1.24, or when you need the structural guarantee that its internal lock can never be held across I/O (no exported `Lock()`). Pick a sharded map (`orcaman/concurrent-map/v2`, or N manual `xsync.MapOf` shards keyed by `hash(key) % N`) for extreme write-heavy, high-cardinality workloads. Never use `sync.Map` for compound transactional updates — it's only atomic per-operation.

See [Mutexes and Concurrent Maps](references/mutexes-and-concurrent-maps.md) for code for all three.

---

## golang.org/x/sync Toolkit, conc, and sync.Once

| Package | Use for |
|---|---|
| `errgroup` | Parallel work with first-error cancellation propagation — panics still crash the process |
| `sourcegraph/conc` | Same as errgroup, but panic-safe (repropagated on `Wait()`) and includes bounded pools |
| `semaphore` | Bounding concurrency (N-at-a-time worker limits) |
| `singleflight` | Collapsing duplicate concurrent calls for the same key (cache stampede prevention) |
| `sync.Once` | One-time initialization, zero lock after the first call |

Reach for `conc` over `errgroup` when goroutines call code you don't fully trust not to panic, or you want bounded concurrency without a separate `semaphore`. `singleflight`'s panic propagation fans out to every waiter on that key — wrap the inner function with `recover` if panics are possible — and `group.Forget(key)` clears a key for re-execution.

See [Concurrency Toolkit](references/concurrency-toolkit.md) for code for errgroup, conc, singleflight, and sync.Once.

---

## Rung 7: Lock-Free Queues and Ring Buffers

Only reach here after profiling shows a genuine MPMC queue is the bottleneck — never start here. Preferred: `Workiva/go-datastructures` (bounded ring buffer, blocking semantics, `Dispose()` for shutdown). Alternative: `golang-design/lockfree` (unbounded, no `Dispose()` — own your shutdown coordination). A lock-free queue is **not** a substitute for a mutex guarding mutable struct fields — that's copy-on-write (Rung 3) or `RWMutex` (Rung 4).

See [Lock-Free Structures](references/lock-free-structures.md) for code and the "what this doesn't fix" detail.

---

## Diagnosis Workflow

Don't guess at contention — prove it first:

1. `golang-profiling` skill → capture `mutex` and `block` pprof profiles under load.
2. `go tool pprof -top -cum mutex.prof` — find which call path holds locks waiters queue behind.
3. Identify the rung: single value (2), read-mostly struct (3), concurrent map (5), or queue (7)?
4. Apply the narrowest fix. Re-profile to confirm contention moved.

---

## Anti-Patterns

```go
// ❌ Lock held across HTTP call — serializes all callers for full I/O duration
mu.Lock()
resp, err := http.Get(url)
mu.Unlock()

// ❌ Lock held across git subprocess
mu.Lock()
out, _ := exec.Command("git", "status").Output()
mu.Unlock()

// ❌ Thundering herd — all misses fetch independently; use singleflight
mu.RLock(); entry, ok := cache[key]; mu.RUnlock()
if !ok { entry = expensiveFetch(key) }

// ❌ sync.Mutex for a read-dominated map — use sync.Map or xsync.MapOf
type Registry struct { mu sync.Mutex; m map[string]Handler }

// ❌ Mutating a value loaded from atomic.Value — concurrent readers hold the same pointer
v := val.Load().(MyStruct)
v.Field = x // RACE
val.Store(v)

// ❌ One global RWMutex over a large map of independent entities — shard it
type SessionManager struct { mu sync.RWMutex; sessions map[string]*Session }

// ❌ Unbounded goroutine-per-request — use errgroup or semaphore
for _, req := range requests {
    go handle(req)
}
```

---

## Libraries Reference

| Library | Import | Use Case |
|---------|--------|----------|
| `sync/atomic` | stdlib | Primitive atomics (Int64, Bool, Pointer, Value) |
| `sync` | stdlib | Mutex, RWMutex, Map, Once, Pool |
| `golang.org/x/sync/errgroup` | x/sync | Parallel work with first-error cancellation |
| `sourcegraph/conc` | third-party | Panic-safe goroutine pools, bounded concurrency, `iter.Map`/`ForEach` |
| `golang.org/x/sync/semaphore` | x/sync | N-at-a-time concurrency bounding |
| `golang.org/x/sync/singleflight` | x/sync | Request coalescing / cache stampede prevention |
| `puzpuzpuz/xsync/v4` | third-party | Typed lock-free map (`xsync.MapOf`) |
| `orcaman/concurrent-map/v2` | third-party | Sharded map for write-heavy workloads |
| `Workiva/go-datastructures` | third-party | Bounded MPMC ring buffer with `Dispose()` |
| `golang-design/lockfree` | third-party | Unbounded lock-free queue/stack |

---

## Related Skills

| Skill | When to apply |
|---|---|
| `golang-development` | General idiomatic Go — this skill assumes that baseline |
| `golang-profiling` | Capture and read CPU/mutex/block profiles before picking a concurrency fix |
| `code-refactoring` | Structural changes once the concurrency fix is chosen |

Files in this skill

  • SKILL.md12 KB
  • references/atomics-and-copy-on-write.md4.2 KB
  • references/concurrency-toolkit.md2.9 KB
  • references/lock-free-structures.md1.2 KB
  • references/mutexes-and-concurrent-maps.md3.6 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…