Absorbed content from rodin/golang-conventions and rodin/prometheus-conventions into a sources/ directory. Reference material — descriptive, not prescriptive. Part of taxonomy cleanup (elixir-patterns issue #4).
13 KiB
Go Language Source: Architectural Conventions
How does the Go team build Go itself? What does the language source reveal about conventions, governance, and infrastructure decisions?
Repo: golang/go
1. Repo Shape
| Metric | Value |
|---|---|
| Size | 632M |
| Go files | 11,245 |
| Assembly files (runtime) | 200 |
| Commits | 66,142 |
| Contributors | 2,842 |
| Test files | 1,811 |
| Non-test files | 6,065 |
| Test ratio | 1:3.3 |
| TODOs (non-test) | 3,428 (all owner-attributed) |
Organizational Philosophy
src/
├── cmd/ # The toolchain (compile, link, go, gofmt, vet)
│ └── compile/
│ └── internal/ssa/ # 417,686 lines of SSA compiler
├── internal/ # 61 hidden packages (API firewall)
├── runtime/ # 129,141 lines (scheduler, GC, memory)
├── encoding/ # Serialization (json, xml, gob)
├── net/ # Networking
├── io/ # Stream interfaces
└── testing/ # Test framework
The Go source has three layers:
- Flat stdlib — user-visible packages (
fmt,io,net) internal/— shared infrastructure hidden from users (61 packages)cmd/— the toolchain itself (compiler, linker, build tool)
2. What the Codebase Values
By import frequency
| Package | Imports | Role |
|---|---|---|
fmt |
2,031 | Formatting (the universal tool) |
testing |
1,658 | Tests are first-class citizens |
strings |
1,454 | String manipulation |
os |
1,306 | System interaction |
unsafe |
1,304 | Low-level memory access |
runtime |
970 | Runtime introspection |
io |
924 | Stream abstraction |
The surprise: unsafe (1,304 imports) ranks 5th — nearly tied with
os. The language that preaches memory safety uses unsafe extensively
in its own implementation. This is the "know the rules so you know
where they don't apply" principle.
By size (what consumes the most lines)
| Component | Lines | Nature |
|---|---|---|
| SSA compiler | 417,686 | Mostly generated rewrite rules |
| Runtime | 129,141 | Scheduler + GC + memory |
testing/ |
4,770 | Testing framework |
internal/poll |
5,087 | OS I/O multiplexing |
internal/fuzz |
4,220 | Fuzzing infrastructure |
encoding/json/v2 |
6,387 | The JSON rewrite (experiment) |
3. The Bootstrap Problem
How does Go compile itself?
Go has been self-hosting since May 2015 (Russ Cox, commit 0f4132c907:
"all: build and use go tool compile, go tool link"). The bootstrap
chain:
- A pre-built Go 1.4 binary compiles the bootstrap tools
- Those tools compile the current Go toolchain
- The new toolchain recompiles itself for correctness
The cmd/dist tool orchestrates this multi-stage build. Unlike
Elixir (which keeps 33 Erlang files permanently), Go eliminated its
non-Go dependencies entirely. The only external requirement is a
previous Go binary.
Convention: Self-hosting means the compiler, linker, and runtime are all written in Go. The runtime includes 200 assembly files for architecture-specific operations (scheduler context switches, atomic operations, system calls).
4. TODO Culture: Owned Accountability
// TODO(gri) — 320 occurrences (Robert Griesemer)
// TODO(mdempsky) — 198 occurrences (Matthew Dempsky)
// TODO(adonovan) — 170 occurrences (Alan Donovan)
// TODO(mknyszek) — 98 occurrences (Michael Knyszek)
// TODO(rsc) — 96 occurrences (Russ Cox)
3,428 TODOs in non-test code. Every TODO has an owner. The top 5 TODO authors are all core team members (compiler, runtime, and tools leads).
Convention: // TODO(username): description
Growth pattern: linkname directives — 43 touches in 2019, 48 in 2022, 92 in 2024, 72 in 2025. The codebase is actively evolving its relationship with internal/external boundaries.
Contrast with Elixir: Go TODOs are permanent documentation of known limitations. Elixir TODOs are time-bombs with version deadlines. Go accepts technical debt as a layer; Elixir refuses to let it accumulate.
5. Unique Patterns
5.1 internal/ as API Firewall (61 packages)
Go's most distinctive structural pattern. The internal/ directory is
enforced by the compiler — code outside the tree cannot import these
packages.
Key internal packages:
internal/godebug— runtime feature flags (79 settings)internal/goexperiment— compile-time experiment guardsinternal/singleflight— dedup concurrent function callsinternal/bisect— binary search for debugging (Russ Cox, 2023)internal/poll— OS I/O polling (5,087 lines)internal/fuzz— fuzzing coordinator (4,220 lines)internal/coverage— code coverage (3,821 lines)
Convention: Code shared between stdlib packages but not suitable for
public API goes in internal/. This is Go's answer to "how do you
share utilities without committing to backward compatibility."
5.2 GODEBUG: Runtime Feature Flags
Introduced 2021 (Brad Fitzpatrick), formalized as a compatibility mechanism by Russ Cox in 2022 (proposal #56986: "extended backwards compatibility for Go", 70 comments).
var http2server = godebug.New("http2server")
func ServeConn(c net.Conn) {
if http2server.Value() == "0" {
// disable HTTP/2
}
}
79 godebug settings across the codebase. Each time a non-default
setting causes a behavior change, code calls IncNonDefault() to
increment a counter readable via runtime/metrics.
Convention: When a behavior change might break existing programs,
add a GODEBUG setting. The old behavior remains accessible via
GODEBUG=setting=old_value. New Go versions automatically use the
old behavior when building code that declared an older go directive
in go.mod.
This is the Go compatibility promise made machine-enforceable.
5.3 GOEXPERIMENT: Compile-Time Feature Gates
// In internal/goexperiment/flags.go:
// GOEXPERIMENT=jsonv2 enables the new JSON API
Convention: Major API additions ship behind experiment flags.
encoding/json/v2 (6,387 lines, Apr 2025) exists in the tree but is
only visible when GOEXPERIMENT=jsonv2 is set. This allows the code
to be developed in-tree, tested by adventurous users, and refined
before the compatibility promise applies.
5.4 Compiler Directives as Hidden Language
//go:linkname localFunction remote/package.Function
//go:nosplit
//go:nowritebarrier
//go:noescape
//go:systemstack
1,711 go:linkname directives and 2,428 runtime compiler
directives in non-test code. These are effectively a hidden language
within Go — they bypass the type system, calling conventions, and
garbage collector safety for performance-critical paths.
Convention: Directives are ONLY acceptable in the runtime and
compiler. The Go team is actively trying to reduce go:linkname usage
(92 touches in 2024 — many are removals). Third-party packages that use
go:linkname to access internals are explicitly unsupported.
5.5 Generated Code: The SSA Compiler
The SSA (Static Single Assignment) compiler backend is 417,686 lines, but the largest files are generated:
opGen.go — 97,135 lines (generated)
rewriteAMD64.go — 79,703 lines (generated)
rewritegeneric.go — 38,337 lines (generated)
rewriteARM64.go — 26,203 lines (generated)
Convention: Generated files are checked into the repo (not
generated at build time). They contain a // Code generated header.
The generators live alongside their output. This means git blame
works on generated code — you can trace when a rewrite rule was added.
5.6 The Runtime: 129K Lines of Go + Assembly
proc.go — 8,156 lines (THE goroutine scheduler)
malloc.go — 2,501 lines (memory allocator)
mgc.go — 2,315 lines (garbage collector)
mheap.go — 3,030 lines (heap management)
panic.go — 1,788 lines (panic/recover machinery)
The scheduler (proc.go) documents its own design at the top:
"The main concepts are: G - goroutine. M - worker thread, or machine. P - processor, a resource that is required to execute Go code."
Convention: The runtime combines Go and assembly (200 .s files).
Architecture-specific operations (context switches, atomic ops, system
calls) are in assembly. Everything else is in Go, using compiler
directives to bypass safety checks where needed.
6. PR Discussion Patterns
Go uses GitHub issues for proposals, not PRs for discussion. The key governance mechanism is the proposal process with designated reviewers.
json/v2 (Issue #71497, 201 comments, Jan 2025)
"The largest major revision of a standard Go package to date."
Key design decision: Split into encoding/json/v2 (semantic) and
encoding/jsontext (syntactic). The syntactic layer has no reflection
dependency — it's a pure JSON tokenizer.
Ship strategy: Land behind GOEXPERIMENT=jsonv2, iterate with
community feedback, then graduate. The code lives in-tree but doesn't
count as a compatibility commitment until the experiment flag is
removed.
External validation: Built as github.com/go-json-experiment/json
first, iterated for years, then proposed for stdlib. The implementation
preceded the proposal.
GODEBUG (Issue #56986, 70 comments, Nov 2022)
Russ Cox's proposal for machine-enforced backward compatibility.
The problem: Sort algorithm changes, bug fixes, and behavior improvements can break programs that depend on old behavior.
The solution: go.mod's go directive becomes a compatibility
declaration. Programs built with Go 1.22 but declaring go 1.20
automatically get Go 1.20 behavior for any setting that changed between
1.20 and 1.22.
Lesson: The Go team solved "how do you improve a language without breaking users" with a general mechanism rather than case-by-case migration. Each new GODEBUG setting is a structured backward compatibility opt-out.
Generics (Issue #15292, 874 comments, 2016-2021)
5 years of discussion. The most debated language change in Go's history.
Lesson: Committee-driven projects can take years on foundational decisions because consensus requires addressing every edge case. Compare to Elixir's formatter (1 hour, zero comments) — the BDFL model moves faster but accepts more single-point-of-failure risk.
slog (Issue #56345, 841 comments, 2022-2023)
Structured logging took 10 months and 841 comments to land.
Lesson: Even when the need is clear and the solution is well-known, Go's process requires exhaustive discussion. The result is usually better (slog is well-designed), but the cost is measured in months.
7. Cross-Ecosystem Comparisons
| Aspect | Go | Elixir |
|---|---|---|
| TODOs | 3,428, owner-attributed, permanent | 127, version-gated, deadlines |
| Self-hosting | Complete since 2015 | 33 Erlang files permanently |
| Feature gates | GOEXPERIMENT (compile-time) | None (ship or don't) |
| Compat mechanism | GODEBUG (79 settings) | Deprecation → removal on version |
| Governance | Committee (proposals, 874-comment threads) | BDFL (José, 1-hour merges) |
| Internal boundary | internal/ (compiler-enforced, 61 packages) |
OTP applications (convention-enforced) |
| Generated code | Checked in (97K-line files) | Compile-time (no artifacts) |
| Assembly | 200 .s files in runtime | None (delegates to Erlang/BEAM) |
| Biggest file | 97,135 lines (generated) | 7,102 lines (Kernel) |
8. What This Teaches
-
internal/solves "shared but not public" at the language level. 61 packages that other ecosystems have to solve with conventions, Go solves with compiler enforcement. This is why Go projects rarely have "utils" packages that leak abstraction — the pattern exists in the language itself. -
GODEBUG is the most sophisticated backward compatibility mechanism in any language runtime. It makes the compatibility promise machine-verifiable rather than socially-enforced. Programs don't just get old behavior by default — they get it because their
go.moddeclares what era they belong to. -
GOEXPERIMENT enables fearless iteration in the stdlib. json/v2 can exist in-tree, be tested, be refined — all without triggering the compatibility promise. This is "feature flags for a language."
-
3,428 TODOs is honest, not sloppy. Each one has an owner. They document known limitations rather than hiding them. Go prefers "explicitly imperfect" over "implicitly broken."
-
Compiler directives are a hidden language. 4,139 directives (linkname + runtime pragmas) bypass Go's safety model. The Go team accepts that the runtime needs a different language than users — but actively restricts this power from escaping to third-party code.
-
Generated code checked in > generated at build time for archaeology.
git blameonrewriteAMD64.gotells you when a codegen rule was added and why. Build-time generation loses this history. -
Committee governance produces better designs but 10x slower. Go's slog (841 comments, 10 months) vs Elixir's formatter (0 comments, 1 hour). The designs are comparable in quality — the process cost is where they differ.
-
unsafein Go's own source (1,304 imports) proves that safety rules are for users, not for runtime implementors. The people who wrote the safety rules know exactly where they don't apply.