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 onsrc/cmd/go/internal/work/,src/runtime/proc.go,src/context/,src/slices/, andsrc/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’sBuilder.Do()for parallel DAG execution. Creates exactlycfg.BuildPworker goroutines (default =runtime.NumCPU()). - Example:
src/cmd/go/internal/work/exec.go:120,224-237—b.readySema = make(chan bool, len(all))acts as both a semaphore and a wakeup signal. Workers loop: receive fromreadySema, 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:3528—findRunnable()callsstealWork(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.Contextparameters; 683select {blocks;case <-ctx.Done()is the dominant shutdown pattern throughout the codebase. - Example:
src/database/sql/ctxutil.go:21— everysql.DBoperation multiplexes work completion againstctx.Done()in select.src/context/context.goitself implements the tree of cancellable contexts using a channel that is lazily created on firstDone()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 funclaunches; 390sync.WaitGroupusages. The dominant multi-goroutine launch pattern throughout stdlib and tools. - Example:
src/internal/fuzz/fuzz.go:174-201— createsopts.Parallelworkers each pulling from a shared work queue, with async.WaitGroupfor teardown.src/cmd/go/internal/work/exec.go:214,225uses the same shape for the build worker pool. - Assessment: Textbook. The stdlib consistently avoids goroutine leaks: every
go funcis paired with either a WaitGroupDone, a channel send at completion, or a context that cancels it.
GC background worker pool (lock-free)#
- Usage: In
src/runtime, the GC runsgcBgMarkWorkergoroutines that park themselves on a lock-free stack (gcBgMarkWorkerPoolof typelfstack) when idle. - Example:
src/runtime/runtime2.go:1471—gcBgMarkWorkerPool lfstack; popped atsrc/runtime/proc.go:3551insidefindRunnable(). 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.Onceusages across all subsystems. - Example:
src/context/context.go—cancelCtx.donechannel is created on the firstDone()call using async.Once-equivalentatomic.CompareAndSwapPointer.src/net,src/os,src/cryptoall usesync.Oncefor one-time global setup. - Assessment: Idiomatic. The stdlib prefers
sync.Onceoverinit()side effects for deferred initialization.
Categories checked#
| Category | Present | Notes |
|---|---|---|
| Worker pools | Yes | Bounded semaphore-channel pool in cmd/go; GC worker pool in runtime |
| Fan-out/fan-in | Yes | Parallel build, fuzz worker launch; stdlib test helpers |
| Pipeline processing | Yes | Compiler’s 7-phase pipeline (sequential, not channel-based) |
| Context cancellation | Yes | Ubiquitous — 2382 context.Context uses |
| Graceful shutdown | Partial | signal.Notify appears in examples/tests; cmd/go uses WaitGroup drain |
| Rate limiting | Minimal | time.NewTicker in a few places; no rate.Limiter in production code |
Error handling#
- Style: Mixed — sentinel errors and custom error types dominate;
fmt.Errorf %wwrapping is common in mid-level packages; rawerrors.Newfor package-level sentinel values. - Error types defined:
io/fs.PathError— wraps Op + Path + Err; used byosfor all filesystem errorscompress/flate.ReadError/WriteError— carry the underlying error with contexthtml/template.Error— includesErrorCodeint for categorized template errorscmd/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 useerrors.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:34—ErrHeader = errors.New("archive/tar: invalid tar header")(package prefix in message is a stdlib convention) - Custom type:
src/io/fs/fs.go:264—type PathError struct { Op, Path string; Err error }withUnwrap() errorfor chain traversal - Wrapping:
src/archive/tar/writer.go:57—fmt.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
- Sentinel:
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.FlagSetregistered on eachbase.Command. Global flags (-C,-v) parsed before subcommand dispatch. All environment variables (GOPATH, GOPROXY, GOTOOLCHAIN, etc.) normalized viaenvcmd.MkEnv(). - Runtime config: Environment variables only —
GODEBUG,GOMAXPROCS,GOMEMLIMIT. Parsed at startup byinternal/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,-gcflagsetc. 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:50—func init()appends subcommands tobase.Go.Commandsslice. Each internal package’sinit()registers its command. This gives compile-time exhaustiveness (missing an import = missing command).src/cmd/compile/main.go:28—archInits = map[string]func(*ssagen.ArchInfo){...}— architecture selection is a runtime table lookup keyed byGOARCH, 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(viasync.Mapalternatives). - Style: Constraint-based with
~underlying-type constraints. Theslicespackage is the canonical example:func Equal[S ~[]E, E comparable](s1, s2 S)— the~[]Econstraint accepts any named slice type, not just[]E. - Range-over-function iterators:
src/iter/iter.go:21—type Seq[V any] func(yield func(V) bool)andtype Seq2[K, V any] func(yield func(K, V) bool). Introduced in Go 1.23. Consumed byslices.All,maps.Keys, etc. Represents a deliberate design choice: iterators are functions, not objects withNext()/Value()methods. - Interning with generics:
src/unique/handle.go:20—type 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 (
slicesreplacessort.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, andencoding/json. - Example:
src/cmd/compile/internal/ir/— virtually every compiler pass iterates overir.Nodeand dispatches viaswitch 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/io—io.ReadWriterembedsReader + Writer;io.ReadWriteCloseraddsCloser. Thecontext.Contextimplementation 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
iopackage’s composable interfaces are cited as the canonical Go design exemplar.
init()-based registry#
- Usage: Subcommand registration in
cmd/go— each subcommand package has aninit()that appends its*base.Commandto the command tree. Architecture init table in the compiler. - Example:
src/cmd/go/internal/run/run.go:69—func init() { work.AddBuildFlags(CmdRun, work.DefaultBuildFlags) }. 15+init()functions acrosscmd/gointernal packages. - Assessment: Acceptable for CLI tools where the set of commands is fixed at compile time. Relies on blank imports in
main.goto 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 ingc.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/gocompiles 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.Actioncarries abuildIDderived 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:132—func (a *Action) BuildActionID() stringreturns 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
archInitsmap, SSA rewrite rules as generated tables, opcode dispatch tables in the runtime. - Example:
src/cmd/compile/main.go:28—archInits = map[string]func(*ssagen.ArchInfo){...}with one entry per architecture.src/cmd/compile/internal/ssa/compile.go:210—passes []passtable 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.