GUIDE — Go under the Discipline, v0.1
01Status. Beta; fifth language guide. Sections isomorphic to the Rust/TS/Python/C++ guides — diff them across languages; that is the genre's contract.
02Framing note — the fifth point of the typology. Rust enforces; TypeScript permits but compiles; Python trusts; C++ demands a subset to survive; Go prescribes. The language ships with its discipline pre-installed: gofmt ended formatting debates, the compiler rejects unused imports, errors are values by culture, inheritance does not exist, internal/ is compiler-enforced encapsulation. The guide's job therefore inverts — not subset selection but gap closure: Go's opinions stop exactly one step short of contract grade in four places, and this guide closes them: (1) errors are values but their sets are open; (2) interface conformance is silent; (3) goroutines are unowned by design; (4) init() blesses the side-effectful import. Patterns were dissolved by the language designers before we arrived: Strategy = interface + composition root; Decorator = wrapper; Observer = channel/callback seam; Visitor degrades to enum + switch (no sum types — the deepest gap, §4); Singleton = forbidden, and http.DefaultClient is its stdlib disguise.
03Scope honesty. Services, CLIs, and long-lived system code. Throwaway scripts are out of scope. cgo is boundary-module territory — cells are pure Go (checkable). Code generated by go generate follows the generated-code rule: the generator's input is the taggable unit, outputs are excluded.
0. Language baseline
- 04Version floor: Go 1.24, target latest stable. Modules with committed
go.sum;GOFLAGS=-mod=readonlyin CI (the lockfile is native — A2 by default).go.workis the workspace analog for multi-module repos. - Formatting is language-owned: gofmt is non-negotiable and costs the Discipline zero budget — the one language where the style war was won upstream.
- Gates:
go vetMUST; staticcheck (MIT) MUST;govulncheck(BSD-3) in CI (supply-chain floor). golangci-lint as the aggregation harness — license flag: GPL-3.0; legal under the Charter's policy only as a separate-process dev tool, never vendored or linked; if even tool-level GPL is unwanted, invoke staticcheck + vet + individual linters directly. - Suppression policy (xfail-strict by construction): bare
//nolintis banned; only//nolint:<linter> // <reason>is legal, and golangci-lint'snolintlint(require-specific + require-explanation + flag-unused) makes stale or vague suppressions a build failure — the suppression registry shrinks truthfully, same mechanism as@ts-expect-errorand pyright's unnecessary-ignore. - Race detector gates tests:
go test -raceis the MUST configuration for any package that starts a goroutine; findings are failures. - Generics: legal and bounded — type parameters for containers/algorithms in infra packages; domain seams stay interface-based unless a measured hot path says otherwise. No type-parameter theater (R-021).
- Boundary validation (parse, don't validate): JSON decoding is loose by default — missing fields become zero values silently, unknown fields are ignored. Boundary decode uses
DisallowUnknownFieldsplus explicit validation; boundary DTO structs convert explicitly into domain types; absent-vs-zero ambiguity is resolved with pointer fields or a validation layer at the boundary, never guessed in cells.
1. Cells
05A cell is a package under internal/cells/<name> — internal/ makes non-registry imports a compile error from outside the module, and the in-module sibling ban is checked at T-syn from the import graph.
- 06Import-is-execution, Go edition:
init()and blank imports. Go's stdlib itself blesses registration-at-import (database/sqldrivers, image codecs,_ "net/http/pprof"), which is exactly why the rule must be explicit:init()and blank imports are banned in cells. The single carve-out is boundary adapters that wrap stdlib-style driver registration — registration happens there or in the composition root, never as a side effect of importing domain code. Package-levelvarwith non-constant initializers is banned in cells for the same reason. - No ambient state: cells never touch
http.DefaultClient, the globallog/slogdefault,os.Getenv,flag.CommandLine,math/randglobals, ortime.Nowdirectly. Capabilities are injected at construction — and Go makes this uniquely cheap: a cell declares the narrow interface it needs privately (type clock interface{ Now() time.Time }) and structural typing does the rest. No central capability package required; no mocking framework either — tests hand in literal implementations. context.Contextis the cancellation capability: first parameter of every potentially-blocking seam method, never stored in a struct field.- Exports are the surface: a cell package exports its constructor (
New(...)) and nothing else beyond seam-required types. Exported-but-unreferenced identifiers are findings. - Promotion to a separate module on the usual triggers (heavy optional deps, independent release cadence, ~2 kLoC).
07Cell manifest (directive carrier, §5) plus the conformance assertion:
08//spec:implements spec://org.vibevm.core/vibevm/modules/vibe-resolver/PROP-003#solver-upgrade r=2
//spec:cell seam=DepSolver variant=sat replaces=naive flag=solver
type SatDepSolver struct{ /* ... */ }
var _ resolver.DepSolver = (*SatDepSolver)(nil) // silent conformance made loud — MUST
func New(p resolver.DepProvider, log *slog.Logger) *SatDepSolver { /* ... */ }
2. Seams
- 09Product seams are central; capability interfaces are consumer-side. The seam (the flag-selectable, replaceable contract) lives in a neutral package; the Go idiom "define interfaces where they're consumed" is kept for injected capabilities inside cells. This split resolves the cultural collision instead of overruling it.
- "Accept interfaces, return structs": constructors return the concrete
*SatDepSolver; only the registry hands out the seam interface. - Every cell carries the compile-time conformance assertion (
var _ Seam = (*Impl)(nil)); conform checks its presence (T-syn) — structural typing stops being silent. - Seam methods that can fail return
(T, error)where the error belongs to the seam's closed error set (§4); values crossing seams are plain structs with useful zero values or explicit constructors — no half-initialized exports.
3. Registry and flags
10R-001 binding — flag at the seam, never in the veins:
11// internal/registry — the only flag reader and the only package
// permitted to import cell packages.
func DepSolver(cfg Config, p resolver.DepProvider, log *slog.Logger) resolver.DepSolver {
switch cfg.Solver { // provenance: default | env | cli | lockfile
case SolverSat:
return satdepsolver.New(p, log)
default:
return naivedepsolver.New(p, log)
}
}
- 12Two tiers, never confused: build tags (
//go:build) answer "is the code in the binary" — the cargo-feature analog, with per-file granularity — and are confined to registry/adapter files, never inside cell bodies (T-lex); runtime flags answer "is the cell selected", read once into a config struct inmain. - Delivery-mode honesty: Go has no credible lazy in-process loading (the
pluginpackage is platform- and version-locked); eager is the only mode, presence is the build tier's job. - No self-registration (now impossible in cells —
init()is banned), no DI frameworks, no reflection-based wiring. The registryswitchis the system's table of contents.
4. Errors as contract
13Go already made errors values; the Discipline makes their sets part of the contract:
- 14Each seam owns a closed, enumerated error set:
15type SolveErrorCode int
const (
ErrCycle SolveErrorCode = iota + 1
ErrUnsatisfiable
)
type SolveError struct {
Code SolveErrorCode
Spec string // violated REQ URI: "spec://...#req-acyclic"
Err error // wrapped cause
}
func (e *SolveError) Error() string { /* renders message + Spec */ }
func (e *SolveError) Unwrap() error { return e.Err }
16Consumers use errors.As against the published type and switch on Code; rendering at the boundary appends the REQ URI (PROP-014 §2.6).
- 17Banned at seams: matching on error strings;
fmt.Errorfwithout%w(breaks the chain); returning anonymouserrors.Newfor expected failures;errorreturns that are sometimes nil-with-meaning. - Exhaustiveness — the deepest gap: Go has no sum types and no exhaustive
switch. Closed sets are const-enums, and theexhaustivelinter (evidence provider) supplies what the compiler won't — the fifth binding of the same Discipline rule, and the only one carried entirely by a linter. - panic = invariant violation — the analog is native, same word.
recoveris legal only at goroutine/boundary top level (e.g. middleware), never as control flow in cells; panicking on expected failures is banned. - Structured concurrency by ownership: every goroutine a cell starts has an owner —
errgroup.Group(BSD-3) orWaitGroup+ context cancellation; nakedgowith cell-outliving lifetime is banned; channels are owned and closed by their spawner. The unowned goroutine is Go's unreferencedcreate_task, with no GC to even cancel it. - Release map for free: every Go binary carries
runtime/debug.ReadBuildInfo— VCS revision, dirty flag, module versions — readable from the artifact itself (go version -m). The A1 chain binary → build info → specmap@commit → REQ needs zero extra machinery; the only rule is not to strip what the runtime gave you (panic stacks stay symbolized, or symbolized copies are retained).
5. specmark carrier
18Directive comments, not doc-comment tags — a deliberate divergence from the TS/Python choice, forced by the toolchain: since Go 1.19 gofmt reformats doc comments and could re-wrap prose tags, but preserves //name:value directive lines verbatim, and godoc hides them. Go already owns the cultural slot (//go:generate, //go:embed); the Discipline takes //spec::
19//spec:implements <uri> r=<N> one edge per line; lines repeat
//spec:deviates <uri> r=<N> reason="..." reason mandatory
//spec:verifies <uri> r=<N> above Test/Fuzz functions
//spec:scope <uri> r=<N> in the package doc block (doc.go) — package-level inheritance
20Parsed via go/ast comment maps; gofmt-proof by construction. The trade-off is named honestly: provenance disappears from rendered godoc and lives in vibe explain/the ledger instead. ≤3 edges per item or split.
6. Naming (R-020/R-021 bindings)
- 21Canonical cell type name is computed:
{Variant}{Seam}→SatDepSolver; the package is the lower-case variant (satdepsolver). Linted against the manifest. - Forbidden in cells regardless of elegance — the Go theater list:
init()and blank imports (§1); reflection-based wiring and struct-tag DSLs in domain code (tags are for boundary DTOs);interface{}/anyin domain signatures where a type or small interface fits; method sets split across files to obscure a type; clever channel topologies as API (channels are implementation, seams are methods);recoveras control flow; package-level mutable state;unsafeoutside designated boundary files.
7. Replacement protocol (R-040 binding)
22A cell with replaces= ships a differential oracle on native fuzzing (go test -fuzz corpus + deterministic seeds in CI): one fuzz target drives both cells through the seam and asserts agreement modulo a documented divergence list, //spec:verifies-tagged, run with -race. Golden files live in testdata/ and follow the promotion protocol — the conventional -update flag never runs in CI, and a local update carries a debt/intent reference. xfail honesty: Go has no native strict-xfail; t.Skip on a known-failing test is banned (skip hides both regressions and healings) — known failures live only in tests-baseline.json, which carries full weight here, the one language in the set without an in-source twin.
8. Risk table (what conform must cover for Go)
| Footgun | Rule | Tier |
|---|---|---|
init() / blank import / non-const package var in a cell |
§1 | T-syn |
ambient default used in a cell (http.DefaultClient, global log, os.Getenv, time.Now) |
§1 | T-syn |
naked go with cell-outliving lifetime; channel without owner |
§4 | T-syn + review |
context.Context stored in a struct / not first param |
§1 | T-syn (vet) |
error-string matching; fmt.Errorf without %w at a seam |
§4 | T-syn |
| expected failure outside the seam's closed error set | §4 | T-sem |
| non-exhaustive switch on a closed const-enum | §4 | T-sem (exhaustive) |
| typed-nil stuffed into an interface | §2 | T-sem (staticcheck) |
missing var _ Seam = (*Impl)(nil) conformance assertion |
§2 | T-syn |
t.Skip on a known-failing test |
§7 | T-lex + test-gate |
| sibling-cell import; cell imported outside the registry | R-002 | T-syn |
//go:build product tag inside a cell body |
§3 | T-lex |
bare //nolint / stale suppression |
§0 | nolintlint |
unsafe / cgo outside boundary files |
§0, §6 | T-lex |
| flag read outside the registry | R-001 | T-syn |
| public export without own/inherited spec edge | PROP-014 §3.2-6 | T-syn + index |
9. Doc layer
24Godoc comments on every exported identifier (the language's own lint culture already expects this): behavior, the seam's error codes with their REQ URIs, goroutine and channel ownership, context semantics. The //spec: directives sit in the same comment block but stay out of the rendered page; human-facing provenance is the ledger's job. Spec stays thin; duplication between godoc and spec is a defect on the spec side.
25First carrier note. No Go exists in vibevm; no carrier is designated. The guide ships genre-complete and unexercised, under the carrier-relative house clause: rules remain DRAFT until a first Go carrier exists, and at that carrier's first milestone any rule without a conform check (or explicit WISH mark) is removed rather than carried as aspiration.