# Go Concurrency — Derived Application > Applies Atelier's domain principles to Go's concurrency specifically. Go's distinctive strength (goroutines, channels, context) earns a dedicated concurrency doc rather than a `go-async.md`. > Derives from `domains/` docs; introduces no new P-rules (D-063). > See `languages/go.md` for the language first-principles stub. ## Goroutines and Structured Concurrency (Concurrency P1 Immutability by Default, C6 Composability) - **`go f()` spawns a goroutine; ensure it does not outlive its parent:** an unstructured `go f()` leaks when the parent returns. Use `sync.WaitGroup`, `errgroup.Group`, or a `context`-scoped pattern to bound lifetime. - **`errgroup.WithContext` for structured concurrency:** a `Group` cancels its context on first error; siblings see the cancellation and exit. Mirrors `TaskGroup` semantics cross-language. - **Goroutines share only immutable inputs:** `go process(snap)` where `snap` is a copy. A goroutine sharing a mutable slice with the parent is a race (Concurrency P1 Immutability, P6 No Silent Races). - **No `go` in a library function without a documented lifetime:** a library that spawns unbounded goroutines leaks them into the caller. Either accept a `context.Context` or return a `Stop()` method. ```go import "golang.org/x/sync/errgroup" func fetchAll(ctx context.Context, ids []string) ([]*User, error) { g, ctx := errgroup.WithContext(ctx) results := make([]*User, len(ids)) for i, id := range ids { i, id := i, id // capture loop vars g.Go(func() error { u, err := fetchUser(ctx, id) if err != nil { return err } results[i] = u return nil }) } if err := g.Wait(); err != nil { return nil, err } return results, nil } ``` ## Channels: Bounded Queues and Backpressure (Concurrency P9 Bounded Queues, C6 Composability) - **Bounded channels apply backpressure:** `make(chan T, N)` blocks the sender when full (Concurrency P9 — bounded queues). Unbounded `make(chan T)` lets the producer run ahead and OOM. - **`select` with `default` for non-blocking send/receive:** a `default` case makes the channel a queue with try semantics; without it, the operation blocks. - **Close channel from the sender, never the receiver:** closing a channel signals "no more sends." A receiver closing it is a race; the sender may still be writing. - **One channel, one responsibility:** do not multiplex control and data on the same channel. Use a `select` over multiple channels instead. - **Applies `messaging/queues`:** a bounded Go channel is an in-process broker — bounded buffer, backpressure, at-most-once handoff. The same semantics apply; the broker is local. ```go func pipeline(ctx context.Context, in <-chan Job, out chan<- Result) { for { select { case j, ok := <-in: if !ok { return } r := process(j) select { case out <- r: case <-ctx.Done(): return } case <-ctx.Done(): return } } } // bounded: backpressure when out is full out := make(chan Result, 16) ``` ## context.Context for Cancellation (Concurrency P7 Cancellation Support, Concurrency P8 Timeout Discipline) - **`context.Context` is the first parameter of every I/O function:** `func fetchUser(ctx context.Context, id string) (*User, error)`. A function that does I/O without a `ctx` cannot be cancelled (Concurrency P7). - **`context.WithTimeout` for a deadline:** `ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel()`. Every external call races against a deadline (Concurrency P8). - **`cancel()` always called, even on success:** `defer cancel()` immediately after creating the context. A leaked context leaks its timer. - **Never store a `context.Context` in a struct:** pass it as a parameter. A struct holding a `ctx` captures a request-scoped value into a long-lived object. - **Applies `concurrency/P7`:** cancellation propagates via `ctx.Done()`. A `select` on `<-ctx.Done()` is the cancel-aware wait. ```go func fetchWithTimeout(ctx context.Context, url string) (*Response, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := http.DefaultClient.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, ErrTimeout } return nil, err } return resp, nil } ``` ## select and Multiplexed Channels (Concurrency P7 Cancellation Support, C6 Composability) - **`select` multiplexes channel operations:** it picks a ready case at random (fair). A `select` with `<-ctx.Done()` plus a data case is the cancel-aware wait. - **`default` makes `select` non-blocking:** use for "send if ready, else drop" (a bounded queue with drop-oldest policy). - **`select {}` blocks forever:** a `select{}` with no cases is a permanent block. Use only in a goroutine that should run until the process exits. - **Applies `concurrency/P7`:** the `select` over `ctx.Done()` and a result channel is the canonical cancel pattern. ```go func processUntilCancel(ctx context.Context, jobs <-chan Job) { for { select { case <-ctx.Done(): return case j, ok := <-jobs: if !ok { return } // ... } } } ``` ## sync Primitives and Lock Scope (Concurrency P3 Boundaries are Locks, Concurrency P5 Lock Minimization) - **`sync.Mutex` scoped minimally:** not held across I/O (a `Send` on a channel, an HTTP call). Hold the lock, mutate, release — then do I/O (Concurrency P3 Lock Scope). - **`sync.RWMutex` for read-heavy, `Mutex` for write-heavy:** RWMutex adds overhead; only prefer it when reads dominate by 10x+. - **`sync.Map` for specific cases (append-only, disjoint keys):** not a general `map[K]V` replacement. For most maps, `Mutex` + `map` is clearer and often faster. - **`sync.Once` for one-time init:** `var once sync.Once; once.Do(func(){ init() })`. Idempotent and race-free. - **Applies `concurrency/P5` (lock minimization):** prefer channels over locks; when a lock is needed, hold it for the smallest possible scope. ```go type Cache struct { mu sync.Mutex items map[string]*User } func (c *Cache) Get(id string) (*User, bool) { c.mu.Lock() defer c.mu.Unlock() u, ok := c.items[id] return u, ok } func (c *Cache) Set(id string, u *User) { c.mu.Lock() c.items[id] = u c.mu.Unlock() // explicit unlock before any I/O } ``` ## Race Detection (Concurrency P6 No Silent Races) - **`go test -race` enforces `P6`:** the race detector instruments memory accesses and fails on data races. See `go-tooling.md` for the CI gate. - **Tests must exercise the concurrent path:** a serial test of a `Mutex`-protected map finds no race. Write tests with N goroutines hitting the map under `-race`. - **Applies `concurrency/P6`:** a race detected at test time is a bug fixed; a race undetected is a production heisenbug. ```go func TestCacheConcurrent(t *testing.T) { c := &Cache{items: map[string]*User{}} var wg sync.WaitGroup for i := 0; i < 100; i++ { i := i wg.Add(1) go func() { defer wg.Done() c.Set(strconv.Itoa(i), &User{}) c.Get(strconv.Itoa(i)) }() } wg.Wait() } ``` ## Cross-References - `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here. - `domains/concurrency/first-principles.md` — Concurrency P1, P3, P5, P6, P7, P8, P9 traced throughout. - `domains/messaging/queues.md` — bounded Go channels as in-process brokers; backpressure parallels (IDEATE-40). - `domains/errors/patterns.md` — `errgroup` and error propagation in concurrent code. - `languages/go-types.md` — typed channels carry the named types defined there. - `languages/go-tooling.md` — the `-race` CI gate that enforces Concurrency P6. - `languages/go-testing.md` — concurrent tests that exercise the race detector.