Day 15 — Modules deep dive
Day 15 — Modules deep dive
Stage II · ~3h (theory-heavy)
Goal: Read and maintain go.mod / go.sum with confidence—versions, require / exclude / replace, go mod tidy, minimal version selection (MVS), and semantic import paths—without cargo-culting commands.
Baseline: Go 1.26.x. The go directive in go.mod declares the language/toolchain baseline your module expects. With GOTOOLCHAIN=auto, newer toolchains may download to match—know your policy.
Why this day exists
Modules are how Go code is shared and versioned. Confusion here looks like:
- “It builds on my machine” with mystery
replace
- Broken import paths for v2+ modules
- Bloated or incomplete
go.sum
- Accidental major upgrades
Day 15 makes you literate enough to review dependency changes in PRs.
Theory 1 — Anatomy of go.mod
module example.com/day15
go 1.26
require (
golang.org/x/text v0.21.0
)
require (
// indirect dependencies listed after tidy
golang.org/x/tools v0.28.0 // indirect
)
exclude example.com/bad v1.2.3
replace example.com/foo => ../foo| Directive | Role |
|---|---|
module |
Module path prefix for packages |
go |
Minimum Go language version for this module |
require |
Direct (and after tidy, indirect) deps with versions |
exclude |
Forbid a specific version |
replace |
Local or fork override (dev / emergency) |
retract |
Author marks bad released versions |
go.sum
Checksums of module contents (and go.mod of deps). Commit go.sum. It protects against tampering and unexpected content changes.
go mod verify # check cached modules against go.sumTheory 2 — Versions and pseudo-versions
Semantic versions
v1.2.3
v1.2.4-rc.1
Tags on the dependency’s repository drive versions.
Pseudo-versions
When you depend on an untagged commit:
v0.0.0-20250102150405-abcdef123456
Readable as: base version + timestamp + commit hash prefix.
go get example.com/pkg@master
go get example.com/pkg@abcdef1Theory 3 — Semantic import versioning
For modules at v2 and above, the import path includes the major version suffix:
module example.com/widget/v2
import "example.com/widget/v2/foo"
| Major | Module path form |
|---|---|
| v0/v1 | example.com/widget |
| v2+ | example.com/widget/v2 |
Why: Go treats import path as identity. Different majors can coexist as different paths.
Common breakages
// go.mod says module example.com/widget/v2
import "example.com/widget/foo" // WRONG
import "example.com/widget/v2/foo" // RIGHTTheory 4 — Minimal version selection (MVS)
When building a module, Go selects the minimum version of each dependency that satisfies all require constraints—not the newest possible.
Implications:
- Adding a dependency that needs
lib@v1.5raises the build to at least v1.5
- You do not automatically jump to latest v1.9 unless something requires it
- Reproducible builds depend on the
go.modrequirement graph + sums
go list -m all # selected versions
go list -m -versions example.com/pkgTheory 5 — Everyday commands
| Command | Purpose |
|---|---|
go mod init |
Create module |
go get pkg@version |
Add/upgrade/downgrade |
go get pkg@none |
Remove require |
go mod tidy |
Match requires to imports; prune unused; fill sums |
go mod download |
Prefetch modules |
go mod graph |
Requirement graph |
go mod why pkg |
Why is this in the build? |
go clean -modcache |
Nuclear cache clear (slow rebuilds) |
go mod tidy ritual
After adding/removing imports:
go mod tidy
git diff go.mod go.sumNever hand-edit go.sum. Prefer tidy + get over manual require surgery.
Theory 6 — replace and exclude
Local replace (development)
replace example.com/foo => ../fooOr:
go mod edit -replace example.com/foo=../fooDo not leave local replaces in published modules without reason—they break outsiders.
Fork replace
replace example.com/foo => github.com/you/foo v1.2.3exclude
exclude github.com/bad/lib v1.0.1 // known badTheory 7 — Build list vs your code
go list -f '{{.Path}} {{.Dir}}' ./...
go list -m -json example.com/day15 | headVendor (awareness)
go mod vendor
go build -mod=vendorVendoring copies deps into vendor/. Useful for some enterprise/airgap workflows; optional for this volume. Prefer modules cache for learning.
GOTOOLCHAIN
go env GOTOOLCHAIN
# auto | local | go1.26.3If go.mod requests a newer Go than installed, auto may download a toolchain. For class consistency, install 1.26.x locally.
Worked examples bank
Example A — Fresh module with a real dep
mkdir -p ~/lab/90daysofx/go/day15
cd ~/lab/90daysofx/go/day15
go mod init example.com/day15
go get rsc.io/quote@v1.5.2package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}go mod tidy
cat go.mod
head go.sumExample B — Explain indirect
After tidy, transitive modules appear as // indirect. They are required by your direct deps, not imported by your packages.
Example C — Upgrade and why
go get rsc.io/quote@latest
go list -m all
go mod why rsc.io/samplerExample D — replace demo
mkdir -p /tmp/hellofork
# create a tiny module that your app replaces to
go mod edit -replace rsc.io/quote=/tmp/hellofork(Only as a learning exercise; restore with go mod edit -dropreplace.)
Example E — Reading a PR diff
When reviewing:
- Did
go.modversions change intentionally?
- Did
go.sumchange with them?
- Any unexpected
replace?
- Major version path changes in imports?
Labs
Suggested workspace: ~/lab/90daysofx/go/day15
Lab 1 — Module literacy notebook
mkdir -p ~/lab/90daysofx/go/day15
cd ~/lab/90daysofx/go/day15
go mod init example.com/day15- Add one small external module (
rsc.io/quoteorgolang.org/x/text).
- Use it in code.
- Run
go mod tidy.
- In
NOTES.md, explain every line of yourgo.mod.
- Run
go mod graphand paste a short excerpt.
- Run
go mod why <some-indirect>if any.
Lab 2 — Version bump exercise
go list -m -versions rsc.io/quote
go get rsc.io/quote@<older>
go test ./... # or go run .
go get rsc.io/quote@<newer>Document what changed in go.mod/go.sum.
Lab 3 — remove dependency cleanly
Delete the import, then:
go mod tidyConfirm require removed. Contrast with forgetting tidy.
Lab 4 — v2 path quiz (written)
Without necessarily depending on a v2 module, write answers:
- Why does
module .../v2appear in import paths?
- Can v1 and v2 of the same logical project coexist in one build?
Common gotchas
| Gotcha | Fix |
|---|---|
Not committing go.sum |
Always commit |
Local replace left in repo |
Drop before publish |
| Wrong import for v2 module | Include /v2 |
| Hand-editing go.sum | Use go mod tidy / go get |
| Assuming newest deps always selected | MVS picks minimum satisfying |
Ignoring go directive |
Match team toolchain (1.26 here) |
GOPATH mode myths |
Modules are default |
Checkpoint
- Can explain
module,go,requirelines
- Knows what
go.sumprotects
- Used
go get+go mod tidysuccessfully
- Can read
go mod graph/go mod whylightly
- Understands replace risks
- Semantic import versioning for v2+ stated correctly
Commit
git add .
git commit -m "day15: modules go.mod go.sum literacy"Tomorrow
Day 16 — Table-driven tests: testing package, t.Run subtests, helpers, fixtures, and turning validation/domain code into a regression suite.