Files
atelier/languages/go-concurrency.md
T
Jon Chery 4e433158cd docs(P03): complete language-derived extension — v0.4
---ci---
project: atelier
phase: 3
milestone: v0.4
status: complete
phase_role: execution
phase_tag: v0.3.3
requirements:
  covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105]
  partial: []
---/ci---
2026-08-05 16:07:21 +00:00

8.0 KiB

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.
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.
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.
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.
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.
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.
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.mderrgroup 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.