The Go Programming Language — Patterns#

Sampling note (XL tier): This repository contains 2000+ Go files across four semi-independent subsystems (runtime, compiler, go tool, stdlib). Pattern detection used grep across the full src/ tree; deep reads were focused on src/cmd/go/internal/work/, src/runtime/proc.go, src/context/, src/slices/, and src/iter/. The patterns described here are representative of all four subsystems.


Concurrency patterns#

Bounded goroutine pool with semaphore channel#

  • Usage: Core pattern in cmd/go’s Builder.Do() for parallel DAG execution. Creates exactly cfg.BuildP worker goroutines (default = runtime.NumCPU()).
  • Example: src/cmd/go/internal/work/exec.go:120,224-237b.readySema = make(chan bool, len(all)) acts as both a semaphore and a wakeup signal. Workers loop: receive from readySema, lock, pop an action, unlock, execute.
  • Assessment: Canonical Go pool idiom. The channel capacity equals the total action count, preventing deadlock. WaitGroup (line 214) coordinates shutdown. Idiomatic and efficient.

Work stealing (G-M-P runtime scheduler)#

  • Usage: Not user-land code, but the Go runtime itself (src/runtime/proc.go) is the reference implementation of cooperative work stealing.
  • Example: src/runtime/proc.go:3528findRunnable() calls stealWork(now) after exhausting local run queue and global queue. Each P has its own run queue; spinning M’s steal half a victim P’s queue.
  • Assessment: The canonical reason the stdlib does not need errgroup — goroutine scheduling itself is work-stealing. Demonstrates the philosophy: runtime primitives, not library abstractions, carry concurrency cost.

Context cancellation with select#

  • Usage: 2382 context.Context parameters; 683 select { blocks; case <-ctx.Done() is the dominant shutdown pattern throughout the codebase.
  • Example: src/database/sql/ctxutil.go:21 — every sql.DB operation multiplexes work completion against ctx.Done() in select. src/context/context.go itself implements the tree of cancellable contexts using a channel that is lazily created on first Done() call.
  • Assessment: Perfectly idiomatic. The context package is a textbook consumer of its own interface: small interface (4 methods), propagated as first parameter, never stored in structs.

Fan-out / WaitGroup coordination#

  • Usage: 1632 go func launches; 390 sync.WaitGroup usages. The dominant multi-goroutine launch pattern throughout stdlib and tools.
  • Example: src/internal/fuzz/fuzz.go:174-201 — creates opts.Parallel workers each pulling from a shared work queue, with a sync.WaitGroup for teardown. src/cmd/go/internal/work/exec.go:214,225 uses the same shape for the build worker pool.
  • Assessment: Textbook. The stdlib consistently avoids goroutine leaks: every go func is paired with either a WaitGroup Done, a channel send at completion, or a context that cancels it.

GC background worker pool (lock-free)#

  • Usage: In src/runtime, the GC runs gcBgMarkWorker goroutines that park themselves on a lock-free stack (gcBgMarkWorkerPool of type lfstack) when idle.
  • Example: src/runtime/runtime2.go:1471gcBgMarkWorkerPool lfstack; popped at src/runtime/proc.go:3551 inside findRunnable(). Workers are goroutines that live for the lifetime of the program.
  • Assessment: Specialized runtime pattern — the lock-free stack replaces a mutex-protected queue to avoid scheduler reentry. Not replicable in user code without unsafe, but instructive as a design case study.

Lazy init via sync.Once#

  • Usage: 268 sync.Once usages across all subsystems.
  • Example: src/context/context.gocancelCtx.done channel is created on the first Done() call using a sync.Once-equivalent atomic.CompareAndSwapPointer. src/net, src/os, src/crypto all use sync.Once for one-time global setup.
  • Assessment: Idiomatic. The stdlib prefers sync.Once over init() side effects for deferred initialization.

Categories checked#

CategoryPresentNotes
Worker poolsYesBounded semaphore-channel pool in cmd/go; GC worker pool in runtime
Fan-out/fan-inYesParallel build, fuzz worker launch; stdlib test helpers
Pipeline processingYesCompiler’s 7-phase pipeline (sequential, not channel-based)
Context cancellationYesUbiquitous — 2382 context.Context uses
Graceful shutdownPartialsignal.Notify appears in examples/tests; cmd/go uses WaitGroup drain
Rate limitingMinimaltime.NewTicker in a few places; no rate.Limiter in production code

Error handling#

  • Style: Mixed — sentinel errors and custom error types dominate; fmt.Errorf %w wrapping is common in mid-level packages; raw errors.New for package-level sentinel values.
  • Error types defined:
    • io/fs.PathError — wraps Op + Path + Err; used by os for all filesystem errors
    • compress/flate.ReadError / WriteError — carry the underlying error with context
    • html/template.Error — includes ErrorCode int for categorized template errors
    • cmd/compile/internal/syntax.Error — carries source position (Pos) for compiler diagnostics
    • Hundreds of package-level sentinel vars: archive/tar.ErrHeader, io.EOF, io.ErrUnexpectedEOF, etc.
  • Wrapping approach: 272 uses of fmt.Errorf("...%w", err). Preferred in mid-level packages where callers use errors.Is/errors.As (483 combined uses). Lower-level packages (runtime, internal) avoid wrapping entirely to keep hot paths allocation-free.
  • Examples:
    • Sentinel: src/archive/tar/common.go:34ErrHeader = errors.New("archive/tar: invalid tar header") (package prefix in message is a stdlib convention)
    • Custom type: src/io/fs/fs.go:264type PathError struct { Op, Path string; Err error } with Unwrap() error for chain traversal
    • Wrapping: src/archive/tar/writer.go:57fmt.Errorf("archive/tar: missed writing %d bytes", nb) (no %w since not intended to be unwrapped)
    • Typed unwrap: errors.As(err, &pathErr) pattern used throughout os/fs callers

Key convention: Sentinel error messages carry the package name as a prefix ("archive/tar: ...", "context: ...") — a stdlib discipline that makes errors self-documenting without source context.


Configuration pattern#

  • Approach: Flags-based configuration exclusively. No functional options in the go tool or compiler. The stdlib itself has no application-level configuration.
  • Go tool config: Per-subcommand flag.FlagSet registered on each base.Command. Global flags (-C, -v) parsed before subcommand dispatch. All environment variables (GOPATH, GOPROXY, GOTOOLCHAIN, etc.) normalized via envcmd.MkEnv().
  • Runtime config: Environment variables only — GODEBUG, GOMAXPROCS, GOMEMLIMIT. Parsed at startup by internal/godebug.
  • Compiler config: Extensive -d=<debug-flag> system via a custom debug flag registry; build tags via //go:build.
  • Example: src/cmd/go/internal/work/build.go:243 (init) registers -buildmode, -compiler, -gcflags etc. onto the build command’s FlagSet.
  • Assessment: Consistent with the flag-centric Go philosophy. No YAML/TOML/Viper anywhere in the toolchain.

Dependency injection#

  • Approach: Manual wiring via init() function side effects and package-level variables. No DI framework.
  • Evidence:
    • src/cmd/go/main.go:50func init() appends subcommands to base.Go.Commands slice. Each internal package’s init() registers its command. This gives compile-time exhaustiveness (missing an import = missing command).
    • src/cmd/compile/main.go:28archInits = map[string]func(*ssagen.ArchInfo){...} — architecture selection is a runtime table lookup keyed by GOARCH, populated at compile time via build tags selecting which arch packages are imported.
    • Runtime: runtime.schedinit() manually initializes all subsystems in a fixed order with no abstraction.
  • Assessment: The Go project deliberately avoids DI frameworks and reflection-based wiring. The init()-registration pattern is the closest equivalent, but it is global state — a trade-off accepted because the go tool is a CLI, not a long-lived service.

Other notable patterns#

Generics (Go 1.18+) — exemplary constraint-based design#

  • Prevalence: Moderate — confined to packages explicitly designed for it: slices, maps, cmp, iter, unique, sync (via sync.Map alternatives).
  • Style: Constraint-based with ~ underlying-type constraints. The slices package is the canonical example: func Equal[S ~[]E, E comparable](s1, s2 S) — the ~[]E constraint accepts any named slice type, not just []E.
  • Range-over-function iterators: src/iter/iter.go:21type Seq[V any] func(yield func(V) bool) and type Seq2[K, V any] func(yield func(K, V) bool). Introduced in Go 1.23. Consumed by slices.All, maps.Keys, etc. Represents a deliberate design choice: iterators are functions, not objects with Next()/Value() methods.
  • Interning with generics: src/unique/handle.go:20type Handle[T comparable] struct — a generic interning handle backed by a concurrent ARTful trie (canonMap[T comparable]).
  • Assessment: Conservative adoption — generics appear only where they eliminate concrete duplication (slices replaces sort.Search-based boilerplate) or enable new abstractions (iter.Seq). Absent from the compiler, runtime, and go tool where code generation or interface-based dispatch is preferred.

Type switches — pervasive in compiler and reflection#

  • Usage: 908 type switch occurrences. Concentrated in cmd/compile (IR node dispatch), reflect, and encoding/json.
  • Example: src/cmd/compile/internal/ir/ — virtually every compiler pass iterates over ir.Node and dispatches via switch n := n.(type). This is the Go equivalent of the visitor pattern without double dispatch.
  • Assessment: Idiomatic in AST/IR code. The compiler avoids the visitor pattern (which would require interfaces for each pass) in favor of exhaustive type switches, accepting that adding a new node type requires updating every switch.

Interface embedding — small, composable contracts#

  • Usage: The stdlib defines hundreds of single-method interfaces (io.Reader, io.Writer, io.Closer, etc.) and promotes composition via embedding.
  • Example: src/ioio.ReadWriter embeds Reader + Writer; io.ReadWriteCloser adds Closer. The context.Context implementation uses struct embedding for default no-op impls: type backgroundCtx struct{ emptyCtx }.
  • Assessment: This is the stdlib’s defining design philosophy — small interfaces maximize reuse. The io package’s composable interfaces are cited as the canonical Go design exemplar.

init()-based registry#

  • Usage: Subcommand registration in cmd/go — each subcommand package has an init() that appends its *base.Command to the command tree. Architecture init table in the compiler.
  • Example: src/cmd/go/internal/run/run.go:69func init() { work.AddBuildFlags(CmdRun, work.DefaultBuildFlags) }. 15+ init() functions across cmd/go internal packages.
  • Assessment: Acceptable for CLI tools where the set of commands is fixed at compile time. Relies on blank imports in main.go to trigger registration — a pattern that requires careful import management but avoids runtime reflection.

Compiler pipeline as sequential function calls (not channels)#

  • Usage: The 7-phase compiler pipeline (parse → typecheck → IR → inline → escape → SSA → codegen) is implemented as sequential function calls in gc.Main(), not as a channel pipeline.
  • Example: src/cmd/compile/internal/gc/main.go — each phase is a function call, state is threaded through package globals and passed structs. Parallelism occurs at the package level (cmd/go compiles packages in parallel) not within a single compilation.
  • Assessment: Deliberate — per-phase parallelism within a single package would add complexity for minimal gain given that packages are the unit of parallelism. The channel-pipeline pattern is not appropriate here.

Content-addressed action caching (not a Go pattern per se, but noteworthy)#

  • Usage: cmd/go/internal/work.Action carries a buildID derived from inputs via cryptographic hash. Completed actions write their outputs to $GOCACHE; future builds skip stale-free actions.
  • Example: src/cmd/go/internal/work/action.go:132func (a *Action) BuildActionID() string returns the content hash used for cache lookup.
  • Assessment: The action DAG + content-hash cache combination is the go tool’s most architecturally distinctive feature — a Bazel-like incremental build system with zero external dependencies.

Table-driven everything (not just tests)#

  • Usage: Tables of function pointers / structs are used throughout for dispatch without reflection. The compiler’s archInits map, SSA rewrite rules as generated tables, opcode dispatch tables in the runtime.
  • Example: src/cmd/compile/main.go:28archInits = map[string]func(*ssagen.ArchInfo){...} with one entry per architecture. src/cmd/compile/internal/ssa/compile.go:210passes []pass table drives SSA optimization pass ordering.
  • Assessment: The table-driven style extends far beyond tests in this codebase — it is the preferred Go alternative to OOP dispatch hierarchies.