open atlas
↑ Back to track
Go, zero to senior GO · 04 · 04

Modules and tooling: MVS, go.sum, workspaces, and the CI toolchain

go.mod requires are minimums and MVS picks the maximum of them — deterministic, no solver, no surprise upgrades; go.sum is a checksum ledger, not a lockfile. replace works only in the main module; go.work wires multi-module dev. CI: vet, staticcheck, -race, drift checks.

GO Middle ◷ 16 min
Level
FoundationsJuniorMiddleSenior

Friday afternoon, every pipeline in the org went red at once: verifying github.com/acme/sigutil@v1.4.2: checksum mismatch. Security incident? Almost the opposite. The library’s author had found a bug in the tag, deleted v1.4.2, and re-tagged the fixed commit under the same version — a quiet little rewrite of history. Go refused to play along: go.sum in every consuming repo held the hash of the original v1.4.2, the public checksum database agreed, and the new bytes under an old name failed verification everywhere. The npm-trained half of the team read the error as an outage; the other half read it as the system working — somebody had changed published code without changing the version, and the build said no. The author cut v1.4.3, one line of go get later everything was green, and the postmortem’s only action item was a link to the MVS docs: in Go, nothing about your dependency graph moves unless you move it.

MVS: minimal version selection, not a solver

Why does your Go build reproduce identically on a colleague’s machine six months later, while the same npm project needs a lockfile and still sometimes differs? The answer is a single algorithm that fits on a napkin.

A require line in go.mod is a minimum, not a range. To build the final version list, Go walks the module graph, collects every module’s required minimum for each dependency, and picks the maximum of the minimums — the smallest version that satisfies everyone. That is the whole algorithm:

// your module requires:  A v1.2.0,  B v1.1.0
// A requires:            C v1.3.0
// B requires:            C v1.5.0
//
// MVS selects C v1.5.0 — the max of the required minimums.
// NOT C's latest release v1.9.2: MVS never jumps ahead of
// what something in the graph explicitly asked for.
//
// Upgrades are always explicit:
//   go get C@v1.6.0    // raise your own minimum
//   go get -u ./...    // raise everything (deliberately)
//   go mod tidy        // sync go.mod with actual imports

Contrast with npm’s model: semver ranges (^1.3.0) resolved at install time mean the same package.json yields different trees on different days, so a lockfile must be bolted on to restore determinism. MVS needs no lockfile for version selection — the build list is a pure function of the go.mod files in the graph, computable by hand, identical on every machine, forever. The tradeoff is honest: you do not get automatic bugfix pickup; a security patch in C v1.5.1 reaches you only when you or a dependency asks for it — hence govulncheck and scheduled go get -u PRs as the counterweight. What go.sum adds is not version pinning but content verification: a ledger of cryptographic hashes for every module version ever used, cross-checked against the sum.golang.org transparency log on first download. Versions come from go.mod; go.sum guarantees the named version is byte-identical to what the rest of the world got — that is what fired in the Hook.

Quiz

Your app requires A and B. A requires zap v1.21.0, B requires zap v1.24.0, and zap's latest release is v1.27.0. Which version builds, and when does it change?

replace and go.work: local wiring that must not leak

Two tools redirect where module source comes from, with very different blast radii. A replace directive in go.mod (replace github.com/acme/lib => ../lib) applies only when that module is the main module — replaces in your dependencies’ go.mod files are ignored entirely. That makes replace safe for forks and emergency pins, but also a classic landmine: a committed => ../lib builds on your laptop and dies in Docker and in every consumer, because the relative path exists only on your disk. Since Go 1.18 the better tool for multi-module development is the workspace: a go.work file listing use ./app and use ./lib makes the toolchain resolve lib from local source across all listed modules — no go.mod edits, and go.work is conventionally untracked (developer-local wiring, like an IDE config), so nothing leaks into the published module. CI builds without a workspace see exactly what consumers will see; GOFLAGS=-mod=readonly (the default behavior since 1.16) plus go.work absence in CI is the honest configuration.

The CI toolchain: vet, staticcheck, -race, generate

The compiler accepts plenty of wrong programs; the toolchain narrows the gap in layers. go vet runs analyzers for bugs the type system misses — printf argument mismatches, copied locks (copylocks), unreachable code; a vet subset already runs automatically during go test, but CI should run the full go vet ./.... staticcheck goes an order of magnitude further with its SA-series checks (nil-map writes, time.Format misuse, deferred Close on a nil response body) and is the de-facto second linter of serious Go shops. The race detector is dynamic instrumentation: go test -race catches only races your tests actually execute, at a real cost — roughly 5–10× CPU and 5–10× memory — which is why it belongs on the CI test run (always) and possibly a canary replica, but not on every production pod. go generate is a build-time contract with humans, not the build: it never runs automatically, so generated code (mocks, protobufs, stringer) is committed, and CI guards drift by regenerating and failing on a non-empty git diff — same trick as go mod tidy -diff. For reproducible release binaries: a pinned toolchain go1.24.x directive in go.mod, -trimpath so file paths do not embed the builder’s home directory, and the module proxy plus go.sum doing the rest — same inputs, same bytes.

Quiz

A teammate deletes go.sum to fix a checksum mismatch error and the build goes green. What did the project actually lose?

Recall before you leave
  1. 01
    Explain how MVS picks versions, why Go needs no lockfile for that, and the precise division of labor between go.mod and go.sum.
  2. 02
    Design the Go toolchain gates for a service repo's CI: what runs, what each catches, and what stays out of production.
Recap

Go’s dependency story is deterministic by construction. Require lines are minimums; MVS gathers every module’s minimums and picks the maximum of them, never jumping to latest — so the build list is a pure function of go.mod files, reproducible on any machine without a lockfile, and nothing moves until go get moves it. The cost is no automatic bugfix pickup, offset by govulncheck and scheduled upgrade PRs. go.sum is the other half and the other job: a ledger of content hashes cross-checked against the public transparency log, which is why a silently re-tagged version fails every consumer’s build — the Friday incident from the Hook was the mechanism succeeding. Local wiring has sharp edges: replace applies only in the main module and a committed relative-path replace breaks Docker and every consumer, while go.work gives multi-module development the same effect as developer-local, conventionally untracked configuration. The toolchain then layers what the compiler cannot promise: go vet and staticcheck for statically detectable bugs, go test -race for dynamically executed races at 5–10× cost (CI yes, prod fleet no), go generate as a human-triggered step whose outputs are committed and drift-checked, and go mod tidy -diff keeping the manifest honest. Reproducible binaries close the loop: pinned toolchain, -trimpath, verified modules — same inputs, same bytes. Now when you see a checksum mismatch in CI or a colleague deletes go.sum to “fix” it, you will know exactly what the error was telling you — and why the fix is one version bump, not a ledger deletion.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.