Back to Backend Development

go-concurrency-patterns

GoConcurrencyGoroutinesChannelsSyncContextBackendAI Agent
36.8k📄 MIT🕒 2026-06-16Source ↗

Install this skill

npx skills add wshobson/agents

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

Go concurrency patterns offer structured approaches to building high-performance, non-blocking systems. The methodology emphasizes the language's core mantra of communicating through channels rather than locking shared memory blocks. By composing goroutines with orchestration tools like WaitGroups, Select statements, and Context propagation, developers can build scalable architectures that handle asynchronous task distribution cleanly. This skill focuses on translating abstract design patterns—such as producer-consumer queues, task pipelines, and fan-out/fan-in processing—into executable Go code. These patterns solve the common challenges of resource starvation, race conditions, and improper lifecycle management. Implementing these established conventions ensures that background tasks remain responsive to cancellation signals and maintain memory safety without the complexity of traditional low-level locking mechanisms.

When to Use This Skill

  • Building high-throughput API gateways or request processors
  • Executing batch processing jobs that require parallel data transformation
  • Creating background task runners that respect graceful shutdown requests
  • Handling streaming data ingestion from multiple sources simultaneously

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • how do I implement a worker pool in Go?
  • create a fan-out fan-in pipeline pattern for my data processing
  • how to handle graceful shutdown for multiple concurrent goroutines
  • best practices for communication between goroutines using channels
  • show me how to use select with context for task cancellation

Pro Tips

  • 💡Always favor channels for communication over shared memory access when possible, adhering to Go's concurrency mantra.
  • 💡Utilize `context.Context` effectively for managing deadlines, cancellations, and propagating request-scoped values across goroutines.
  • 💡Before reaching for `sync.Mutex` or `sync.WaitGroup`, consider if a channel-based solution offers a more idiomatic and safer approach to concurrency.

What this skill does

  • Orchestrating concurrent worker pools to process job queues
  • Implementing data pipelines using fan-out and fan-in stages
  • Managing task lifecycles through context cancellation and deadlines
  • Multiplexing multiple channel operations using select statements
  • Synchronizing parallel execution via atomic primitives and WaitGroups

When not to use it

  • Low-complexity scripts where synchronous execution is sufficient
  • Memory-constrained environments where excessive goroutine spawning causes overhead
  • Applications requiring complex shared state where atomic primitives might be too slow

Example workflow

  1. Define the data structures for incoming jobs and processing results
  2. Initialize a context with cancellation to manage worker lifecycles
  3. Spawn a fixed set of worker goroutines reading from a shared channel
  4. Dispatch work items into the job channel from the main thread
  5. Wait for all workers to finish using sync.WaitGroup before closing result channels
  6. Collect and process results from the output channel until it drains

Prerequisites

  • Basic knowledge of Go syntax
  • Understanding of pointers and memory allocation
  • Familiarity with standard library packages like sync and context

Pitfalls & limitations

  • !Creating goroutine leaks by failing to close channels or signal cancellation
  • !Deadlocking applications by incorrectly using channel reads or writes
  • !Race conditions caused by accidentally sharing pointers between goroutines without synchronization

FAQ

Why prefer channels over mutexes?
Channels explicitly manage the ownership of data transfer, which aligns with Go's philosophy of sharing by communicating, significantly reducing the surface area for race conditions.
What happens if I forget to close a channel?
Recipients will wait indefinitely for more data, leading to a goroutine leak and potential deadlocks when the program expects the pipeline to terminate.
When is it better to use a Mutex?
Mutexes are typically more efficient than channels when you need to protect specific fields within a struct from concurrent read/write access.

How it compares

Generic prompts often provide buggy or unsafe threading code; this skill provides production-tested architectural templates that guarantee safe memory access and proper cleanup.

Source & trust

37k stars📄 MIT🕒 Updated 2026-06-16
📄 Full skill instructions — original source: wshobson/agents
# Go Concurrency Patterns

Production patterns for Go concurrency including goroutines, channels, synchronization primitives, and context management.

## When to Use This Skill

- Building concurrent Go applications
- Implementing worker pools and pipelines
- Managing goroutine lifecycles
- Using channels for communication
- Debugging race conditions
- Implementing graceful shutdown

## Core Concepts

### 1. Go Concurrency Primitives

| Primitive | Purpose |
| ----------------- | -------------------------------- |
| goroutine | Lightweight concurrent execution |
| channel | Communication between goroutines |
| select | Multiplex channel operations |
| sync.Mutex | Mutual exclusion |
| sync.WaitGroup | Wait for goroutines to complete |
| context.Context | Cancellation and deadlines |

### 2. Go Concurrency Mantra

Don't communicate by sharing memory;
share memory by communicating.


## Quick Start

package main

import (
"context"
"fmt"
"sync"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

results := make(chan string, 10)
var wg sync.WaitGroup

// Spawn workers
for i := 0; i < 3; i++ {
wg.Add(1)
go worker(ctx, i, results, &wg)
}

// Close results when done
go func() {
wg.Wait()
close(results)
}()

// Collect results
for result := range results {
fmt.Println(result)
}
}

func worker(ctx context.Context, id int, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()

select {
case <-ctx.Done():
return
case results <- fmt.Sprintf("Worker %d done", id):
}
}


## Patterns

### Pattern 1: Worker Pool

package main

import (
"context"
"fmt"
"sync"
)

type Job struct {
ID int
Data string
}

type Result struct {
JobID int
Output string
Err error
}

func WorkerPool(ctx context.Context, numWorkers int, jobs <-chan Job) <-chan Result {
results := make(chan Result, len(jobs))

var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
default:
result := processJob(job)
results <- result
}
}
}(i)
}

go func() {
wg.Wait()
close(results)
}()

return results
}

func processJob(job Job) Result {
// Simulate work
return Result{
JobID: job.ID,
Output: fmt.Sprintf("Processed: %s", job.Data),
}
}

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

jobs := make(chan Job, 100)

// Send jobs
go func() {
for i := 0; i < 50; i++ {
jobs <- Job{ID: i, Data: fmt.Sprintf("job-%d", i)}
}
close(jobs)
}()

// Process with 5 workers
results := WorkerPool(ctx, 5, jobs)

for result := range results {
fmt.Printf("Result: %+v\n", result)
}
}


### Pattern 2: Fan-Out/Fan-In Pipeline

package main

import (
"context"
"sync"
)

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

// Stage 2: Square numbers (can run multiple instances)
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 <-ctx.Done():
return
case out <- n * n:
}
}
}()
return out
}

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

// Start output goroutine for each input channel
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
select {
case <-ctx.Done():
return
case out <- n:
}
}
}

wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// Close out after all inputs are done
go func() {
wg.Wait()
close(out)
}()

return out
}

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

// Generate input
in := generate(ctx, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Fan out to multiple squarers
c1 := square(ctx, in)
c2 := square(ctx, in)
c3 := square(ctx, in)

// Fan in results
for result := range merge(ctx, c1, c2, c3) {
fmt.Println(result)
}
}


### Pattern 3: Bounded Concurrency with Semaphore

package main

import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
"sync"
)

type RateLimitedWorker struct {
sem *semaphore.Weighted
}

func NewRateLimitedWorker(maxConcurrent int64) *RateLimitedWorker {
return &RateLimitedWorker{
sem: semaphore.NewWeighted(maxConcurrent),
}
}

func (w *RateLimitedWorker) Do(ctx context.Context, tasks []func() error) []error {
var (
wg sync.WaitGroup
mu sync.Mutex
errors []error
)

for _, task := range tasks {
// Acquire semaphore (blocks if at limit)
if err := w.sem.Acquire(ctx, 1); err != nil {
return []error{err}
}

wg.Add(1)
go func(t func() error) {
defer wg.Done()
defer w.sem.Release(1)

if err := t(); err != nil {
mu.Lock()
errors = append(errors, err)
mu.Unlock()
}
}(task)
}

wg.Wait()
return errors
}

// Alternative: Channel-based semaphore
type Semaphore chan struct{}

func NewSemaphore(n int) Semaphore {
return make(chan struct{}, n)
}

func (s Semaphore) Acquire() {
s <- struct{}{}
}

func (s Semaphore) Release() {
<-s
}


### Pattern 4: Graceful Shutdown

package main

import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)

type Server struct {
shutdown chan struct{}
wg sync.WaitGroup
}

func NewServer() *Server {
return &Server{
shutdown: make(chan struct{}),
}
}

func (s *Server) Start(ctx context.Context) {
// Start workers
for i := 0; i < 5; i++ {
s.wg.Add(1)
go s.worker(ctx, i)
}
}

func (s *Server) worker(ctx context.Context, id int) {
defer s.wg.Done()
defer fmt.Printf("Worker %d stopped\n", id)

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
// Cleanup
fmt.Printf("Worker %d cleaning up...\n", id)
time.Sleep(500 * time.Millisecond) // Simulated cleanup
return
case <-ticker.C:
fmt.Printf("Worker %d working...\n", id)
}
}
}

func (s *Server) Shutdown(timeout time.Duration) {
// Signal shutdown
close(s.shutdown)

// Wait with timeout
done := make(chan struct{})
go func() {
s.wg.Wait()
close(done)
}()

select {
case <-done:
fmt.Println("Clean shutdown completed")
case <-time.After(timeout):
fmt.Println("Shutdown timed out, forcing exit")
}
}

func main() {
// Setup signal handling
ctx, cancel := context.WithCancel(context.Background())

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

server := NewServer()
server.Start(ctx)

// Wait for signal
sig := <-sigCh
fmt.Printf("\nReceived signal: %v\n", sig)

// Cancel context to stop workers
cancel()

// Wait for graceful shutdown
server.Shutdown(5 * time.Second)
}


### Pattern 5: Error Group with Cancellation

package main

import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
)

func fetchAllURLs(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)

results := make([]string, len(urls))

for i, url := range urls {
i, url := i, url // Capture loop variables

g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("creating request for %s: %w", url, err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetching %s: %w", url, err)
}
defer resp.Body.Close()

results[i] = fmt.Sprintf("%s: %d", url, resp.StatusCode)
return nil
})
}

// Wait for all goroutines to complete or one to fail
if err := g.Wait(); err != nil {
return nil, err // First error cancels all others
}

return results, nil
}

// With concurrency limit
func fetchWithLimit(ctx context.Context, urls []string, limit int) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(limit) // Max concurrent goroutines

results := make([]string, len(urls))
var mu sync.Mutex

for i, url := range urls {
i, url := i, url

g.Go(func() error {
result, err := fetchURL(ctx, url)
if err != nil {
return err
}

mu.Lock()
results[i] = result
mu.Unlock()
return nil
})
}

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

return results, nil
}


### Pattern 6: Concurrent Map with sync.Map

package main

import (
"sync"
)

// For frequent reads, infrequent writes
type Cache struct {
m sync.Map
}

func (c *Cache) Get(key string) (interface{}, bool) {
return c.m.Load(key)
}

func (c *Cache) Set(key string, value interface{}) {
c.m.Store(key, value)
}

func (c *Cache) GetOrSet(key string, value interface{}) (interface{}, bool) {
return c.m.LoadOrStore(key, value)
}

func (c *Cache) Delete(key string) {
c.m.Delete(key)
}

// For write-heavy workloads, use sharded map
type ShardedMap struct {
shards []*shard
numShards int
}

type shard struct {
sync.RWMutex
data map[string]interface{}
}

func NewShardedMap(numShards int) *ShardedMap {
m := &ShardedMap{
shards: make([]*shard, numShards),
numShards: numShards,
}
for i := range m.shards {
m.shards[i] = &shard{data: make(map[string]interface{})}
}
return m
}

func (m *ShardedMap) getShard(key string) *shard {
// Simple hash
h := 0
for _, c := range key {
h = 31*h + int(c)
}
return m.shards[h%m.numShards]
}

func (m *ShardedMap) Get(key string) (interface{}, bool) {
shard := m.getShard(key)
shard.RLock()
defer shard.RUnlock()
v, ok := shard.data[key]
return v, ok
}

func (m *ShardedMap) Set(key string, value interface{}) {
shard := m.getShard(key)
shard.Lock()
defer shard.Unlock()
shard.data[key] = value
}


### Pattern 7: Select with Timeout and Default

func selectPatterns() {
ch := make(chan int)

// Timeout pattern
select {
case v := <-ch:
fmt.Println("Received:", v)
case <-time.After(time.Second):
fmt.Println("Timeout!")
}

// Non-blocking send/receive
select {
case ch <- 42:
fmt.Println("Sent")
default:
fmt.Println("Channel full, skipping")
}

// Priority select (check high priority first)
highPriority := make(chan int)
lowPriority := make(chan int)

for {
select {
case msg := <-highPriority:
fmt.Println("High priority:", msg)
default:
select {
case msg := <-highPriority:
fmt.Println("High priority:", msg)
case msg := <-lowPriority:
fmt.Println("Low priority:", msg)
}
}
}
}


## Race Detection

# Run tests with race detector
go test -race ./...

# Build with race detector
go build -race .

# Run with race detector
go run -race main.go


## Best Practices

### Do's

- **Use context** - For cancellation and deadlines
- **Close channels** - From sender side only
- **Use errgroup** - For concurrent operations with errors
- **Buffer channels** - When you know the count
- **Prefer channels** - Over mutexes when possible

### Don'ts

- **Don't leak goroutines** - Always have exit path
- **Don't close from receiver** - Causes panic
- **Don't use shared memory** - Unless necessary
- **Don't ignore context cancellation** - Check ctx.Done()
- **Don't use time.Sleep for sync** - Use proper primitives

## Resources

- [Go Concurrency Patterns](https://go.dev/blog/pipelines)
- [Effective Go - Concurrency](https://go.dev/doc/effective_go#concurrency)
- [Go by Example - Goroutines](https://gobyexample.com/goroutines)

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/go-concurrency-patterns/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/wshobson/agents/go-concurrency-patterns/SKILL.md
  • Cursor: ~/.cursor/skills/wshobson/agents/go-concurrency-patterns/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/wshobson/agents/go-concurrency-patterns/SKILL.md

🚀 Install with CLI:
npx skills add wshobson/agents

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid backend development issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Backend Development and is published by W. Shobson, maintained in wshobson/agents.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.