Day 15 — Modules deep dive

Updated

July 30, 2026

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.

Note

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.sum

Theory 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@abcdef1

Theory 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" // RIGHT

Theory 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.5 raises 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.mod requirement graph + sums
go list -m all          # selected versions
go list -m -versions example.com/pkg

Theory 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.sum

Never hand-edit go.sum. Prefer tidy + get over manual require surgery.


Theory 6 — replace and exclude

Local replace (development)

replace example.com/foo => ../foo

Or:

go mod edit -replace example.com/foo=../foo

Do not leave local replaces in published modules without reason—they break outsiders.

Fork replace

replace example.com/foo => github.com/you/foo v1.2.3

exclude

exclude github.com/bad/lib v1.0.1 // known bad

Theory 7 — Build list vs your code

go list -f '{{.Path}} {{.Dir}}' ./...
go list -m -json example.com/day15 | head

Vendor (awareness)

go mod vendor
go build -mod=vendor

Vendoring 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.3

If 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.2
package main

import (
    "fmt"

    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
go mod tidy
cat go.mod
head go.sum

Example 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/sampler

Example 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:

  1. Did go.mod versions change intentionally?
  2. Did go.sum change with them?
  3. Any unexpected replace?
  4. 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
  1. Add one small external module (rsc.io/quote or golang.org/x/text).
  2. Use it in code.
  3. Run go mod tidy.
  4. In NOTES.md, explain every line of your go.mod.
  5. Run go mod graph and paste a short excerpt.
  6. 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 tidy

Confirm require removed. Contrast with forgetting tidy.

Lab 4 — v2 path quiz (written)

Without necessarily depending on a v2 module, write answers:

  1. Why does module .../v2 appear in import paths?
  2. 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, require lines
  • Knows what go.sum protects
  • Used go get + go mod tidy successfully
  • Can read go mod graph / go mod why lightly
  • 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.