# Go Concurrency Patterns Patterns extracted from the Go standard library source code. --- ## 1. sync.Mutex — The Basic Lock ### Source: `src/sync/mutex.go:18-34`, `src/sync/mutex.go:42-67` ```go // src/sync/mutex.go:18-34 // A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { _ noCopy mu isync.Mutex } // src/sync/mutex.go:36-39 type Locker interface { Lock() Unlock() } // src/sync/mutex.go:43-46 func (m *Mutex) Lock() { m.mu.Lock() } // src/sync/mutex.go:64-67 func (m *Mutex) Unlock() { m.mu.Unlock() } ``` ### Why - **Zero value is ready to use** — no constructor needed - **Must not be copied** — enforced by `noCopy` field (go vet detects copies) - **Not associated with a goroutine** — one goroutine can Lock, another can Unlock - **Locker interface** — abstracts over Mutex and RWMutex ### Idiomatic Usage ```go var mu sync.Mutex mu.Lock() defer mu.Unlock() // ... critical section ... ``` ### Anti-pattern ```go // DON'T: Copy a mutex type Config struct { mu sync.Mutex data map[string]string } c2 := *c1 // COPIES the mutex — data race waiting to happen // DON'T: Forget defer mu.Lock() // if this panics, the mutex stays locked forever doSomething() mu.Unlock() ``` --- ## 2. sync.Once — Exactly-Once Initialization ### Source: `src/sync/once.go:12-36`, `src/sync/once.go:56-79` ```go // src/sync/once.go:12-23 type Once struct { _ noCopy done atomic.Bool m Mutex } // src/sync/once.go:56-63 func (o *Once) Do(f func()) { if !o.done.Load() { o.doSlow(f) } } // src/sync/once.go:65-72 func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if !o.done.Load() { defer o.done.Store(true) f() } } ``` ### Why The implementation reveals a subtle guarantee: **when Do returns, f has finished**. The naive CAS-only approach (documented in the comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait. The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27). ### Idiomatic Usage ```go var ( instance *DB once sync.Once ) func GetDB() *DB { once.Do(func() { instance = connectToDB() }) return instance } ``` ### Anti-pattern ```go // DON'T: Implement once yourself with a bool var initialized bool var mu sync.Mutex func init() { mu.Lock() if !initialized { // ... setup ... initialized = true } mu.Unlock() } // DON'T: Call Do recursively (deadlocks) var once sync.Once once.Do(func() { once.Do(func() { /* deadlock */ }) }) ``` --- ## 3. sync.WaitGroup — Waiting for Goroutine Completion ### Source: `src/sync/waitgroup.go:14-43`, `src/sync/waitgroup.go:236-260` ```go // src/sync/waitgroup.go:14-43 // A WaitGroup is a counting semaphore typically used to wait // for a group of goroutines or tasks to finish. // // Typically, a main goroutine will start tasks, each in a new // goroutine, by calling WaitGroup.Go and then wait for all tasks to // complete by calling WaitGroup.Wait. For example: // // var wg sync.WaitGroup // wg.Go(task1) // wg.Go(task2) // wg.Wait() type WaitGroup struct { noCopy noCopy state atomic.Uint64 sema uint32 } ``` ### Go 1.25+: WaitGroup.Go ```go // src/sync/waitgroup.go:236-260 func (wg *WaitGroup) Go(f func()) { wg.Add(1) go func() { defer func() { if x := recover(); x != nil { panic(x) // don't call Done — let panic propagate } wg.Done() }() f() }() } ``` ### Why `WaitGroup.Go` (new in Go 1.25) encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit. ### Classic Pattern (pre-Go 1.25) ```go var wg sync.WaitGroup for _, item := range items { wg.Add(1) go func() { defer wg.Done() process(item) }() } wg.Wait() ``` ### Anti-pattern ```go // DON'T: Add inside the goroutine (race with Wait) for _, item := range items { go func() { wg.Add(1) // might run after Wait is called! defer wg.Done() process(item) }() } wg.Wait() // DON'T: Forget Done (Wait blocks forever) wg.Add(1) go func() { process() // forgot wg.Done() }() wg.Wait() // hangs ``` --- ## 4. sync.Pool — Object Reuse for GC Pressure ### Source: `src/sync/pool.go:44-63` ```go // src/sync/pool.go:44-63 // A Pool is a set of temporary objects that may be individually saved and // retrieved. // // Any item stored in the Pool may be removed automatically at any time without // notification. If the Pool holds the only reference when this happens, the // item might be deallocated. // // Pool's purpose is to cache allocated but unused items for later reuse, // relieving pressure on the garbage collector. type Pool struct { noCopy noCopy local unsafe.Pointer localSize uintptr victim unsafe.Pointer victimSize uintptr New func() any } ``` ### Why Pool is **not** a general cache. Items can vanish between GC cycles. It's for reducing allocation pressure on hot paths — `fmt` uses it for print buffers, `encoding/json` for encoder state. ### Idiomatic Usage (from fmt package) ```go var ppFree = sync.Pool{ New: func() any { return new(pp) }, } func newPrinter() *pp { p := ppFree.Get().(*pp) p.reset() return p } func (p *pp) free() { p.buf = p.buf[:0] // reset before returning ppFree.Put(p) } ``` ### Anti-pattern ```go // DON'T: Use Pool for connection pooling (items disappear!) var connPool = sync.Pool{ New: func() any { return connectToDB() }, } // Connections may be GC'd at any time — use database/sql's pool instead // DON'T: Put dirty objects back without resetting pool.Put(buf) // still has data from last use — memory leak or data leak ``` --- ## 5. Channel as Done Signal (Context Pattern) ### Source: `src/context/context.go:83-100` (Done channel), `src/io/pipe.go:42-45` ```go // src/context/context.go:83-100 // Done returns a channel that's closed when work done on behalf of this // context should be canceled. Done() <-chan struct{} // src/io/pipe.go:42-45 type pipe struct { wrMu sync.Mutex wrCh chan []byte rdCh chan int once sync.Once done chan struct{} // closed on pipe close rerr onceError werr onceError } ``` ### Why A `chan struct{}` costs zero bytes per send and closing it broadcasts to all receivers simultaneously. This is the canonical "done" signal in Go. ```go select { case <-ctx.Done(): return ctx.Err() case result := <-work: return result, nil } ``` ### Anti-pattern ```go // DON'T: Use chan bool for done signals done := make(chan bool) // wastes 1 byte per signal, true/false meaningless // DON'T: Send to done (only works once, only one receiver) done <- struct{}{} // only unblocks one goroutine // DO: Close the channel (broadcasts to all) close(done) ``` --- ## 6. Context Propagation ### Source: `src/context/context.go:37-48` (rules), `src/net/http/request.go:368-380` From the package doc: ```go // src/context/context.go:37-48 // Programs that use Contexts should follow these rules: // // Do not store Contexts inside a struct type; instead, pass a Context // explicitly to each function that needs it. The Context should be the first // parameter, typically named ctx: // // func DoSomething(ctx context.Context, arg Arg) error { // // ... use ctx ... // } // // Do not pass a nil Context, even if a function permits it. Pass context.TODO // if you are unsure about which Context to use. ``` ### Request context in net/http: ```go // src/net/http/request.go:368-380 func (r *Request) WithContext(ctx context.Context) *Request { if ctx == nil { panic("nil context") } r2 := new(Request) *r2 = *r r2.ctx = ctx return r2 } ``` ### Why Context flows **down** the call chain, never stored in structs. `WithContext` returns a shallow copy — the original request is not mutated. This is the immutable-context pattern. ### Anti-pattern ```go // DON'T: Store context in a struct type Server struct { ctx context.Context // stale context persists beyond request lifecycle } // DON'T: Pass nil context doWork(nil, data) // use context.TODO() if unsure // DON'T: Put context anywhere other than first parameter func doWork(data Data, ctx context.Context) // wrong position ``` --- ## 7. Context Cancellation (WithCancel/WithTimeout) ### Source: `src/context/context.go:242-249` (WithCancel), `src/net/http/server.go:4007-4050` (TimeoutHandler) ```go // src/context/context.go:242-249 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := withCancel(parent) return c, func() { c.cancel(true, Canceled, nil) } } ``` Real-world use — net/http TimeoutHandler: ```go // src/net/http/server.go:4011-4014 func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) { ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt) defer cancelCtx() r = r.WithContext(ctx) done := make(chan struct{}) // ... go func() { h.handler.ServeHTTP(tw, r) close(done) }() select { case <-done: // handler completed case <-ctx.Done(): // timeout } } ``` ### Why `defer cancelCtx()` is critical — it releases resources (timers, goroutines) when the parent returns, even if the child hasn't timed out yet. The go vet tool checks for this. ### Anti-pattern ```go // DON'T: Forget to call cancel (leaks goroutines) ctx, _ := context.WithCancel(parent) // cancel function discarded! // DON'T: Cancel before work starts ctx, cancel := context.WithTimeout(parent, 5*time.Second) cancel() // immediately cancels — no work can happen doWork(ctx) ``` --- ## 8. Select with Done Channel ### Source: `src/context/context.go:83-100` (Done in select), `src/io/pipe.go:51-60` ```go // src/io/pipe.go:51-60 func (p *pipe) read(b []byte) (n int, err error) { select { case <-p.done: return 0, p.readCloseError() default: } select { case bw := <-p.wrCh: nr := copy(b, bw) p.rdCh <- nr return nr, nil case <-p.done: return 0, p.readCloseError() } } ``` ### Why The double-select pattern: first a non-blocking check (with `default`), then a blocking wait. The non-blocking check prevents a race where `done` was closed between the last operation and the current one. ### Standard Context Select Pattern ```go // From context package doc (line 83-100) func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } } ``` ### Anti-pattern ```go // DON'T: Check ctx.Done() in a busy loop for { select { case <-ctx.Done(): return default: } // busy-spins CPU at 100% } ``` --- ## 9. Goroutine-per-Connection (net/http Server) ### Source: `src/net/http/server.go` (conceptual — the serve loop spawns goroutines per connection) ```go // The pattern (simplified from server.go serve loop): for { conn, err := listener.Accept() if err != nil { // handle continue } go srv.handleConn(conn) // one goroutine per connection } ``` ### Why Go's goroutines are cheap (~2KB initial stack). The server doesn't need a thread pool or async/await — it spawns a goroutine per connection and lets the runtime scheduler handle multiplexing. ### Anti-pattern ```go // DON'T: Limit yourself to a fixed thread pool for I/O-bound work pool := make(chan struct{}, 10) // artificial limit on connections for { pool <- struct{}{} // blocks at 10 conn := accept() go func() { defer func() { <-pool }() handle(conn) }() } // Only appropriate for CPU-bound work or resource-constrained scenarios ``` --- ## 10. Channel as Synchronous Pipe (io.Pipe) ### Source: `src/io/pipe.go:38-45`, `src/io/pipe.go:195-205` ```go // src/io/pipe.go:38-45 type pipe struct { wrMu sync.Mutex wrCh chan []byte // writer sends data slices rdCh chan int // reader returns bytes consumed once sync.Once done chan struct{} rerr onceError werr onceError } // src/io/pipe.go:195-205 func Pipe() (*PipeReader, *PipeWriter) { pw := &PipeWriter{r: PipeReader{pipe: pipe{ wrCh: make(chan []byte), rdCh: make(chan int), done: make(chan struct{}), }}} return &pw.r, pw } ``` ### Why `io.Pipe` connects a Writer to a Reader using **unbuffered channels** — each Write blocks until the corresponding Read consumes the data. No internal buffering means backpressure is automatic. The `done` channel signals when either end closes. ### Pattern: Channel Pipeline ```go func generate(ctx context.Context) <-chan int { out := make(chan int) go func() { defer close(out) for i := 0; ; i++ { select { case out <- i: case <-ctx.Done(): return } } }() return out } ``` ### Anti-pattern ```go // DON'T: Forget to close channels (receivers block forever) func produce() <-chan int { ch := make(chan int) go func() { for i := 0; i < 10; i++ { ch <- i } // forgot close(ch) — receivers hang on range }() return ch } ``` --- ## 11. database/sql Connection Opener Goroutine ### Source: `src/database/sql/sql.go:836-843` ```go // src/database/sql/sql.go:836-843 func OpenDB(c driver.Connector) *DB { ctx, cancel := context.WithCancel(context.Background()) db := &DB{ connector: c, openerCh: make(chan struct{}, connectionRequestQueueSize), lastPut: make(map[*driverConn]string), stop: cancel, } go db.connectionOpener(ctx) return db } ``` ### Why A dedicated background goroutine (`connectionOpener`) processes connection requests from a buffered channel. The goroutine is controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down cleanly. This is the "long-lived worker goroutine with context shutdown" pattern. ### Anti-pattern ```go // DON'T: Start background goroutines without shutdown mechanism go func() { for { processWork() // runs forever, no way to stop it } }() ``` --- ## 12. noCopy — Preventing Value Copies at Vet Time ### Source: `src/sync/cond.go:120-126` ```go // src/sync/cond.go:120-126 type noCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*noCopy) Lock() {} func (*noCopy) Unlock() {} ``` ### Why Embedding `noCopy` in a struct makes `go vet` report an error when the struct is copied. All sync primitives use this because copying a locked mutex or in-use WaitGroup is always a bug. ### Anti-pattern ```go // DON'T: Pass sync types by value func doWork(wg sync.WaitGroup) { // copies the WaitGroup! defer wg.Done() } // DO: Pass by pointer func doWork(wg *sync.WaitGroup) { defer wg.Done() } ``` --- ## Summary: Concurrency Decision Guide | Need | Use | |------|-----| | Protect shared state | `sync.Mutex` + `defer Unlock()` | | One-time initialization | `sync.Once` | | Wait for N goroutines | `sync.WaitGroup` (prefer `.Go()` in 1.25+) | | Reduce allocation pressure | `sync.Pool` (not for connections!) | | Signal completion/cancellation | `chan struct{}` + `close()` | | Deadline/timeout propagation | `context.WithTimeout` / `context.WithCancel` | | Backpressure between producer/consumer | Unbuffered channels | | Fan-out with results | Buffered channel + WaitGroup | | Long-lived background worker | Goroutine + context cancellation | | Prevent struct copying | Embed `noCopy` field |