
Go Concurrency
- 924 installs
- 137 repo stars
- Updated June 20, 2026
- cxuu/golang-skills
go-concurrency is a Claude Code skill that teaches Go developers advanced concurrency patterns from Effective Go for request multiplexing, reply channels, and CPU-bound parallelization without race conditions.
About
go-concurrency is an advanced Go reference skill grounded in Effective Go for situational concurrency patterns beyond basic goroutines. It explains channels of channels, embedding reply channels inside request structs, and patterns for multiplexing many clients through shared workers while keeping responses routed correctly. The skill emphasizes when to use each pattern for request/response fan-in and CPU-bound parallelization instead of reaching for mutexes by default. Backend and systems developers reach for go-concurrency when a Go service must coordinate many concurrent callers, route replies safely, or scale CPU work across goroutines without introducing data races.
- Channels-of-channels pattern for non-blocking RPC with per-request reply channels
- CPU-bound parallelization using sync.WaitGroup across multiple cores
- Reference implementations directly from Effective Go, modernized for current Go
- Situational patterns: apply only when you need request/response multiplexing or true parallelism
Go Concurrency by the numbers
- 924 all-time installs (skills.sh)
- +39 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #427 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cxuu/golang-skills --skill go-concurrencyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 924 |
|---|---|
| repo stars | ★ 137 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 20, 2026 |
| Repository | cxuu/golang-skills ↗ |
How do you multiplex Go requests with reply channels?
Correctly implement request multiplexing, reply channels, and CPU-bound parallelization in Go services without introducing mutexes or race conditions.
Who is it for?
Go backend developers implementing high-concurrency services who need Effective Go patterns for reply channels and worker multiplexing.
Skip if: Beginners learning basic goroutines and channels who have not yet hit request routing or CPU-bound scaling problems.
When should I use this skill?
User asks about Go channels of channels, reply channels, request multiplexing, CPU-bound parallelization, or Effective Go concurrency.
What you get
Go concurrency patterns with reply-channel request structs, multiplexed workers, and race-free CPU parallelization guidance.
- Reply-channel request structs
- Multiplexed worker patterns
- Concurrency design notes
Files
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
// 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()// Bad: No way to stop or wait
go func() { for { flush(); time.Sleep(delay) } }()Test for leaks with 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.
// 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.
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
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) for type-safe atomic operations. Raw int32/int64 fields make it easy to forget atomic access on some code paths.
// 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 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 when passing cancellation, deadlines, or request-scoped values through goroutines
- Error handling: See go-error-handling when propagating errors from goroutines or using errgroup
- Defensive hardening: See go-defensive when protecting shared state at API boundaries or using defer for cleanup
- Interface design: See go-interfaces 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 — Go Time podcast
- go.uber.org/goleak — Goroutine leak
detector for testing
- go.uber.org/atomic — Type-safe
atomic operations
Advanced Concurrency Patterns
Detailed reference for advanced concurrency patterns from Effective Go. These patterns are situational — use when you need request/response multiplexing or CPU-bound parallelization.
---
Channels of Channels
Source: Effective Go
A channel is a first-class value that can be allocated and passed around like any other. A powerful pattern is embedding a reply channel inside a request struct, letting each client provide its own path for the answer:
type Request struct {
args []int
f func([]int) int
resultChan chan int
}The client sends a request with a function, its arguments, and a channel on which to receive the result:
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
clientRequests <- request
fmt.Printf("answer: %d\n", <-request.resultChan)The server handler reads from the queue and sends results back on each request's reply channel:
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}This pattern forms the basis for a rate-limited, parallel, non-blocking RPC system without a mutex in sight.
---
CPU-Bound Parallelization
Source: Effective Go (modernized)
When a computation can be broken into independent pieces, parallelize it across CPU cores using a sync.WaitGroup to wait for completion. On Go 1.25 and newer, use WaitGroup.Go so the add/done bookkeeping stays coupled to the goroutine:
type Vector []float64
func (v Vector) DoSome(i, n int, u Vector) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
}
func (v Vector) DoAll(u Vector) {
numCPU := runtime.NumCPU()
var wg sync.WaitGroup
for i := 0; i < numCPU; i++ {
i := i
wg.Go(func() {
v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u)
})
}
wg.Wait()
}Use runtime.NumCPU() for hardware cores or runtime.GOMAXPROCS(0) to honor the user's resource configuration. For Go versions before 1.25, use wg.Add(1), go func, and defer wg.Done() around each launched goroutine.
Important: Don't confuse concurrency (structuring a program as
independently executing components) with parallelism (executing calculations
simultaneously on multiple CPUs). Go is a concurrent language; not all
parallelization problems fit its model.
---
Common Mistakes
Forgetting to signal completion
If a goroutine never calls wg.Done() (or never sends on a done channel), the waiting goroutine blocks forever:
// Bad: Missing wg.Done — deadlocks
var wg sync.WaitGroup
wg.Add(1)
go func() {
doWork()
}()
wg.Wait()
// Good: Always defer wg.Done
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()Unbounded goroutine spawning
Launching one goroutine per work item with no limit can exhaust memory or overwhelm downstream resources. Use a semaphore to cap concurrency:
// Bad: Spawns len(items) goroutines at once
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
process(it)
}(item)
}
wg.Wait()
// Good: Semaphore limits concurrency to maxWorkers
var wg sync.WaitGroup
sem := make(chan struct{}, maxWorkers)
for _, item := range items {
wg.Add(1)
sem <- struct{}{}
go func(it Item) {
defer wg.Done()
defer func() { <-sem }()
process(it)
}(item)
}
wg.Wait()Buffer Pooling with Channels
Use a buffered channel as a free list to reuse allocated buffers, avoiding repeated allocations. This "leaky buffer" pattern uses select with default for non-blocking operations.
Source: Effective Go
var freeList = make(chan *Buffer, 100) // Buffered channel as free list
// Client: Get buffer from free list or allocate new one
func getBuffer() *Buffer {
select {
case b := <-freeList:
return b // Reuse existing buffer
default:
return new(Buffer) // Free list empty; allocate new buffer
}
}
// Server: Return buffer to free list if room, otherwise drop it
func putBuffer(b *Buffer) {
b.Reset() // Prepare for reuse
select {
case freeList <- b:
// Buffer returned to free list
default:
// Free list full; drop buffer (GC will reclaim)
}
}How It Works
1. Non-blocking receive: Client tries to grab a buffer from freeList. If empty, default runs and allocates a new buffer. 2. Non-blocking send: Server tries to return the buffer. If freeList is full, default runs and the buffer is dropped for garbage collection. 3. Bounded memory: The channel capacity (100) limits pooled buffers, preventing unbounded growth.
This pattern is useful when allocation is expensive and buffer reuse is beneficial, but you don't want blocking behavior when the pool is empty or full.
When to Use
- High-frequency allocations of similar-sized objects
- Performance-critical code paths where allocation overhead matters
- Scenarios where you want bounded memory usage
Production Alternative
For production code, consider sync.Pool which provides similar functionality with better integration into the garbage collector:
var bufferPool = sync.Pool{
New: func() any {
return new(Buffer)
},
}
func getBuffer() *Buffer {
return bufferPool.Get().(*Buffer)
}
func putBuffer(b *Buffer) {
b.Reset()
bufferPool.Put(b)
}sync.Pool advantages:
- Automatic cleanup during garbage collection
- No need to manage pool size
- Thread-safe by design
- Better performance under high concurrency
The channel-based approach is still valuable for understanding Go's concurrency primitives and for cases where you need more control over pool behavior.
Goroutine Lifecycle Patterns
Detailed patterns for managing goroutine lifetimes — ensuring every goroutine has a clear start/stop mechanism and preventing resource leaks.
---
Making Lifetimes Obvious
The WaitGroup example and scoping rules are in the parent skill (SKILL.md §
Goroutine Lifetimes, Core Rules). This reference covers: stop/done channel
patterns, waiting strategies, init() lifecycle examples, and synchronous API
design.
---
Stop/Done Channel Pattern
Every goroutine must have a predictable stop mechanism. Use a stop channel to signal shutdown and a done channel to confirm exit:
var (
stop = make(chan struct{}) // tells the goroutine to stop
done = make(chan struct{}) // tells us that the goroutine exited
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// To shut down:
close(stop) // signal the goroutine to stop
<-done // and wait for it to exitSending on a closed channel panics — always use close() to signal, never send:
ch := make(chan int)
close(ch)
ch <- 13 // panic: send on closed channel---
Waiting for Goroutines
The sync.WaitGroup pattern for multiple goroutines is in the parent skill(SKILL.md § Goroutine Lifetimes). Below is the done-channel alternative for a
single goroutine.
Use a done channel for a single goroutine:
done := make(chan struct{})
go func() {
defer close(done)
// work...
}()
<-done // wait for goroutine to finish---
No Goroutines in init()
The core rule is in the parent skill (SKILL.md § Core Rules, rule 3). Below
are expanded examples showing lifecycle management.
// Bad: Spawns uncontrollable background goroutine
func init() {
go doWork()
}// Good: Explicit lifecycle management
type Worker struct {
stop chan struct{}
done chan struct{}
}
func NewWorker() *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go w.doWork()
return w
}
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}---
Prefer Synchronous Functions
The rationale and benefit table are in the parent skill (SKILL.md §
Synchronous Functions). Below is a concrete code example.
// Good: Synchronous function - caller controls concurrency
func ProcessItems(items []Item) ([]Result, error) {
var results []Result
for _, item := range items {
result, err := processItem(item)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
// Caller can add concurrency if needed:
go func() {
results, err := ProcessItems(items)
// handle results
}()Sync Primitives Patterns
Detailed patterns for mutexes and atomic operations — covering mutex embedding pitfalls and type-safe atomic access.
---
Don't Embed Mutexes
If you use a struct by pointer, the mutex should be a non-pointer field. Do not embed the mutex on the struct, even if the struct is not exported.
// Bad: Embedded mutex exposes Lock/Unlock as part of API
type SMap struct {
sync.Mutex // Lock() and Unlock() become methods of SMap
data map[string]string
}
func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
}// Good: Named field keeps mutex as implementation detail
type SMap struct {
mu sync.Mutex
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}With the bad example, Lock and Unlock methods are unintentionally part of the exported API. With the good example, the mutex is an implementation detail hidden from callers.
---
Atomic Operations: Full Example
The standard sync/atomic package operates on raw types (int32, int64, etc.), making it easy to forget to use atomic operations consistently.
// Bad: Easy to forget atomic operation
type foo struct {
running int32 // atomic
}
func (f *foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race! forgot atomic.LoadInt32
}// Good: Type-safe atomic operations
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running.Load() // can't accidentally read non-atomically
}The atomic.Bool, atomic.Int64, etc. types (available in stdlib sync/atomic since Go 1.19, or via go.uber.org/atomic) add type safety by hiding the underlying type.
---
Channel Direction Examples
Specifying direction prevents accidental misuse:
// Good: Direction specified - clear ownership
func sum(values <-chan int) int {
total := 0
for v := range values {
total += v
}
return total
}// Bad: No direction - allows accidental misuse
func sum(values chan int) (out int) {
for v := range values {
out += v
}
close(values) // Bug! This compiles but shouldn't happen.
}Related skills
How it compares
Pick go-concurrency when you need Effective Go multiplexing and reply-channel architectures rather than introductory goroutine tutorials.
FAQ
What Go patterns does go-concurrency cover?
go-concurrency covers advanced Effective Go patterns including channels of channels, reply channels embedded in request structs, and CPU-bound parallelization. These patterns help route responses per client and scale worker pools without unnecessary mutex usage.
When should I use go-concurrency instead of basic goroutine guides?
go-concurrency fits when a Go service needs request/response multiplexing or CPU-bound parallelization beyond starter goroutine examples. The skill is situational and assumes familiarity with basic channels before applying reply-channel architectures.
Is Go Concurrency safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.