| name | golang-concurrency |
| description | Go concurrency patterns, context propagation, goroutine lifecycle, channel ownership, sync primitives, goroutine leak prevention, and race detector usage. Use when writing or reviewing concurrent Go code.
|
| allowed-tools | Bash(go test -race *), Bash(go vet *), Bash(golangci-lint *), Read, Grep |
Go Concurrency Safety
Concurrency is the most dangerous area in Go. Data races cause undefined behavior.
Goroutine leaks exhaust memory. This skill enforces safe patterns from the start.
The Three Laws of Go Concurrency
- Every goroutine must have a defined exit condition — no fire-and-forget without lifecycle control
- context.Context is the cancellation bus — propagate it, never store it, always check it
- Channels have a single owner — only the sender closes, never the receiver
1. context.Context Rules
✅ Correct patterns
func ProcessOrder(ctx context.Context, orderID string) error { ... }
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
result, err := processOrder(ctx, r.URL.Query().Get("id"))
...
}
func processOrder(ctx context.Context, id string) error {
if err := db.QueryContext(ctx, ...); err != nil { return err }
if err := cache.SetContext(ctx, ...); err != nil { return err }
return nil
}
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := process(ctx, item); err != nil { return err }
}
return nil
}
❌ Anti-patterns
type Service struct {
ctx context.Context
}
func (s *Service) DoWork(ctx context.Context) {
go doThing(context.Background())
}
ctx, _ := context.WithTimeout(parent, 5*time.Second)
func ProductionHandler(ctx context.Context) {
doSomething(context.TODO())
}
2. Goroutine Lifecycle
✅ sync.WaitGroup — basic fan-out
func processAll(ctx context.Context, items []Item) error {
var wg sync.WaitGroup
errs := make(chan error, len(items))
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
if err := process(ctx, item); err != nil {
errs <- err
}
}(item)
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil { return err }
}
return nil
}
✅ errgroup — structured concurrency with errors (preferred)
import "golang.org/x/sync/errgroup"
func processAll(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
return process(ctx, item)
})
}
return g.Wait()
}
✅ Worker pool (bounded concurrency)
func workerPool(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 job, ok := <-jobs:
if !ok { return nil }
if err := process(ctx, job); err != nil { return err }
}
}
})
}
return g.Wait()
}
❌ Goroutine anti-patterns
go doWork()
go func() {
for {
process()
}
}()
for _, item := range items {
go func() {
process(item)
}()
}
3. Channel Ownership & Patterns
✅ Ownership rules
func producer(ctx context.Context) <-chan int {
ch := make(chan int, 10)
go func() {
defer close(ch)
for i := 0; i < 100; i++ {
select {
case <-ctx.Done(): return
case ch <- i:
}
}
}()
return ch
}
func consumer(ctx context.Context, ch <-chan int) {
for {
select {
case <-ctx.Done(): return
case v, ok := <-ch:
if !ok { return }
use(v)
}
}
}
❌ Channel anti-patterns
func badConsumer(ch chan int) {
close(ch)
}
results := make(chan Result)
4. sync Primitives
sync.Mutex / sync.RWMutex
type SafeCache struct {
mu sync.RWMutex
store map[string]string
}
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.store[key]
return v, ok
}
func (c *SafeCache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = value
}
sync.Pool — reduce allocations in hot paths
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func processRequest(data []byte) string {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
buf.Write(data)
return buf.String()
}
sync.Once — safe initialization
type Singleton struct {
once sync.Once
instance *DB
}
func (s *Singleton) DB() *DB {
s.once.Do(func() {
s.instance = connectDB()
})
return s.instance
}
5. Race Detection Commands
go test -race ./...
go build -race -o app_race .
go vet ./...
golangci-lint run --enable=govet,staticcheck --disable-all ./...
grep -rn "go func(" --include="*.go" . | grep -v "_test.go"
grep -rn "\.Wait()" --include="*.go" . | grep -v "_test.go"
6. Goroutine Leak Detection
go get go.uber.org/goleak
curl http://localhost:6060/debug/pprof/goroutine?debug=2 | head -50
Quick Anti-Pattern Checklist
| Check | Command |
|---|
| Data races | go test -race ./... |
| Goroutine fire-and-forget | grep -rn "^go " --include="*.go" . |
| Missing cancel() | grep -rn "WithTimeout|WithCancel|WithDeadline" --include="*.go" . |
| Context in struct | grep -rn "ctx.*context.Context" --include="*.go" . | grep "struct {" |
| Channel closed by receiver | Code review only |