executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
golang-concurrency-patternsGo concurrency patterns for production services: context cancellation, errgroup, worker pools, bounded parallelism, fan-in/fan-out, and common race/deadlock pitfalls
Independent GitHub snapshot · Automated classification · Original documentation governs
name: golang-concurrency-patterns description: "Go concurrency patterns for production services: context cancellation, errgroup, worker pools, bounded parallelism, fan-in/fan-out, and common race/deadlock pitfalls" user-invocable: false disable-model-invocation: true version: 1.0.0 category: toolchain author: Claude MPM Team license: MIT progressive_disclosure: entry_point: summary: "Write race-free, cancellation-aware Go using context, errgroup, channels, and bounded concurrency patterns" when_to_use: "When implementing concurrent pipelines, background workers, fan-out/fan-in, or fixing goroutine leaks, deadlocks, and data races in Go services" quick_start: "1. Thread context everywhere 2. Use errgroup.WithContext for fan-out 3. Bound concurrency with a semaphore 4. Make goroutine lifetimes explicit 5. Test with -race and timeouts" token_estimate: entry: 150 full: 5000 context_limit: 800 tags:
Go concurrency scales when goroutine lifetimes are explicit, cancellation is propagated with context.Context, and shared state is protected (channels or locks). Apply these patterns to build reliable services and avoid common failure modes: goroutine leaks, deadlocks, and data races.
Default building blocks
context to drive cancellation and deadlines.errgroup.WithContext for fan-out/fan-in with early abort.Avoid
time.After inside hot loops.Treat goroutines as resources with a clear owner and shutdown condition.
✅ Correct: stop goroutines via context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// do work
}
}
}()
❌ Wrong: goroutine without a stop condition
go func() {
for {
doWork() // leaks forever
}
}()
✅ Correct: one goroutine owns the map
type req struct {
key string
reply chan<- int
}
func mapOwner(ctx context.Context, in <-chan req) {
m := map[string]int{}
for {
select {
case <-ctx.Done():
return
case r := <-in:
r.reply <- m[r.key]
}
}
}
✅ Correct: mutex protects shared map
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (s *SafeMap) Get(k string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.m[k]
return v, ok
}
errgroup)Use errgroup.WithContext to run concurrent tasks, cancel siblings on error, and wait for completion.
✅ Correct: cancel on first error
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
id := id // capture
g.Go(func() error {
return process(ctx, id)
})
}
if err := g.Wait(); err != nil {
return err
}
❌ Wrong: WaitGroup loses the first error and does not propagate cancellation
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go func() {
defer wg.Done()
_ = process(context.Background(), id) // ignores caller ctx + captures id
}()
}
wg.Wait()
Bound parallelism to prevent CPU/memory exhaustion and downstream overload.
✅ Correct: bounded fan-out
limit := make(chan struct{}, 8) // max 8 concurrent
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
id := id
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
case limit <- struct{}{}:
}
defer func() { <-limit }()
return process(ctx, id)
})
}
return g.Wait()
Use a fixed number of workers for stable throughput and predictable resource usage.
✅ Correct: worker pool with context stop
type Job struct{ ID string }
func runPool(ctx context.Context, jobs <-chan Job, workers int) error {
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < workers; i++ {
g.Go(func() error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case j, ok := <-jobs:
if !ok {
return nil
}
if err := handleJob(ctx, j); err != nil {
return err
}
}
}
})
}
return g.Wait()
}
Prefer one-directional channels and close only from the sending side.
✅ Correct: sender closes
func stageA(ctx context.Context, out chan<- int) {
defer close(out)
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return
case out <- i:
}
}
}
❌ Wrong: receiver closes
func stageB(in <-chan int) {
close(in) // compile error in<-chan; also wrong ownership model
}
time.Ticker vs time.After)Use time.NewTicker for loops; avoid time.After allocations in hot paths.
✅ Correct: ticker
t := time.NewTicker(1 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
poll()
}
}
❌ Wrong: time.After in loop
for {
select {
case <-ctx.Done():
return
case <-time.After(1 * time.Second):
poll()
}
}
errgroup.WithContextsync.WaitGroupRun targeted tests with the race detector and disable caching during debugging:
go test -race ./...
go test -run TestName -race -count=1 ./...
✅ Correct: test-level timeout via context
func TestSomething(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := doThing(ctx); err != nil {
t.Fatal(err)
}
}
Actions:
context.WithTimeout) around blocking operations.ok == false.<-limit release in semaphore patterns.go test -race reports)Actions:
Actions:
ctx.Done().time.Ticker is stopped and channels are closed by senders.context.Background() inside request paths; propagate caller context.errgroup: https://pkg.go.dev/golang.org/x/sync/errgroup