From go-agent-skills
Reviews and implements safe concurrency patterns in Go: goroutines, channels, sync primitives, context propagation, and goroutine lifecycle management. Use when writing concurrent code, reviewing async patterns, or debugging race conditions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/go-agent-skills:go-concurrency-reviewThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Concurrency in Go is powerful and deceptively easy to get wrong.
Concurrency in Go is powerful and deceptively easy to get wrong. These patterns prevent goroutine leaks, data races, and deadlocks.
Pick the mode that matches the request before starting:
go statements and shared state.-race report, deadlock). Start from "Auditing Large Codebases"
and the Race Detection section to localize it.For a full concurrency audit, run these independent passes rather than one linear read:
go statement
(grep -rn "go func\|go [a-zA-Z]" --include="*.go") and verify each
has a termination path (context, closed channel, WaitGroup).context.Context.If your environment supports delegating work to parallel sub-agents or
tasks, assign each pass to one; otherwise run them in order. Findings
must cite file.go:line. Always finish with go test -race ./....
EVERY goroutine MUST have a clear termination path. No fire-and-forget.
errgroup for coordinated goroutines:g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch data: %w", err)
}
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
if err := w.process(job); err != nil {
w.logger.Error("process job", slog.Any("error", err))
}
}
}
}
// ✅ Good — caller controls lifecycle
go worker.Run(ctx)
// ❌ Bad — function secretly starts goroutine
func NewWorker() *Worker {
w := &Worker{}
go w.run() // hidden goroutine — caller has no control
return w
}
// Unbuffered — synchronization point
ch := make(chan Result)
// Buffered with size 1 — single-item handoff
ch := make(chan Result, 1)
// Larger buffers need explicit justification with documented reasoning
ch := make(chan Result, 100) // requires comment explaining why
done := make(chan struct{})
close(done) // broadcast signal to all receivers
func produce(ctx context.Context) <-chan Item {
ch := make(chan Item)
go func() {
defer close(ch)
for {
item, err := fetchNext(ctx)
if err != nil {
return
}
select {
case ch <- item:
case <-ctx.Done():
return
}
}
}()
return ch
}
// ✅ Good — zero value works
type Cache struct {
mu sync.RWMutex
items map[string]Item
}
// ❌ Bad — unnecessary pointer
type Cache struct {
mu *sync.RWMutex // never do this
}
type SafeMap struct {
mu sync.RWMutex // mutex guards the fields below
items map[string]string
count int
}
The mutex should appear directly above the field(s) it protects, with a comment indicating the relationship.
// ✅ Good — minimal lock scope
func (c *Cache) Get(key string) (Item, bool) {
c.mu.RLock()
item, ok := c.items[key]
c.mu.RUnlock()
return item, ok
}
// ✅ Also good — defer for methods that return early
func (c *Cache) GetOrCreate(key string) Item {
c.mu.Lock()
defer c.mu.Unlock()
if item, ok := c.items[key]; ok {
return item
}
item := newItem(key)
c.items[key] = item
return item
}
// ❌ BLOCKER — copying a mutex copies its lock state
cache2 := *cache1 // this copies the mutex!
Use sync/atomic or go.uber.org/atomic for simple counters and flags:
// ✅ Good — type-safe atomics
import "go.uber.org/atomic"
type Server struct {
running atomic.Bool
reqCount atomic.Int64
}
func (s *Server) HandleRequest() {
s.reqCount.Inc()
// ...
}
func (s *Service) Process(ctx context.Context, req Request) error {
// Derive context with timeout for external call
fetchCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // ALWAYS defer cancel
data, err := s.client.Fetch(fetchCtx, req.ID)
if err != nil {
return fmt.Errorf("fetch %s: %w", req.ID, err)
}
// ...
}
// ✅ Good
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ Bad — blocks forever if context cancelled
result := <-ch
// ❌ Bad — mutable global, not safe for concurrent access
var db *sql.DB
// ✅ Good — pass as dependency
type Server struct {
db *sql.DB
}
type Client struct {
initOnce sync.Once
conn *grpc.ClientConn
}
func (c *Client) getConn() *grpc.ClientConn {
c.initOnce.Do(func() {
c.conn = dial()
})
return c.conn
}
ALWAYS run tests with race detector during CI:
go test -race ./...
This is non-negotiable. A test suite that passes without -race proves nothing
about concurrent correctness.
context.Background() used where parent context was availableselect without ctx.Done() case in blocking operationtime.Sleep used for synchronization instead of proper signalinginit() or constructor without lifecycle controlnpx claudepluginhub eduardo-sl/go-agent-skills --plugin go-agent-skillsImplements and reviews Go concurrency patterns: goroutines, channels, sync primitives, worker pools, pipelines. Detects leaks, race conditions, and ownership issues. Operates in write, review, and audit modes.
Guides writing concurrent Go code: goroutines, channels, mutexes, thread-safety, and avoiding data races. Use when parallelizing work or protecting shared state.
Provides code examples for Go goroutines, channels (unbuffered, buffered, directional, closing), select statements, and sync patterns when writing concurrent Go code.