Module & Dependency Rules
Semver, minimal dependencies, and internal package boundaries.
Quick reference for module authors and platform teams governing go.mod hygiene across services and libraries.
How to Use This Cheatsheet
- Scan on every PR touching
go.mod,go.sum, or import paths. - Libraries enforce stricter semver rules than internal
cmd/binaries. - Pair with
govulncheckand dependency update automation. - Monorepos using
go.workstill need per-module tidiness.
go.mod hygiene
| Rule | Command / pattern | Fail signal |
|---|---|---|
| Tidy before merge | go mod tidy | Non-empty diff on go.mod/go.sum |
| Pin Go version | go 1.26 matches CI image | Local/CI toolchain mismatch |
| One module path per repo root (default) | module example.com/foo | Accidental nested modules without go.work |
| Require intentional replaces | replace with comment + issue | Permanent replace to forks |
| Retract bad tags | retract directive in go.mod | Consumers resolve broken pseudo-versions |
| Vendor only with policy | go mod vendor + commit policy | Surprise vendor drift |
Versioning and tags
| Rule | Library | Application / cmd |
|---|---|---|
| Semver git tags | v1.4.2 required | Optional internal tags |
| Breaking API change | Major bump / v2 module path | Refactor freely inside deploy boundary |
| Pre-release | v0.y.z or internal/ | Feature branches |
| Changelog | CHANGELOG.md or release notes | Deploy notes |
| Consumer upgrade | go get example.com/foo@v1.4.2 | Build image pins commit |
Dependency minimization
| Rule | Do | Avoid |
|---|---|---|
| Direct deps | Only what package imports | Copy-paste go get stacks from blog posts |
| Indirect awareness | Review go mod why -m | Ignoring transitive bloat |
| Stdlib first | net/http, encoding/json | Heavy framework for one handler |
| Interface boundaries | Small adapters at edges | Importing ORM into domain packages |
| Test deps | require test-only in separate module or build tags | Production imports of testify in library code |
| License scan | Allow-list licenses in platform policy | Unreviewed AGPL transitive deps |
Package layout boundaries
| Rule | Pattern | Enforcement |
|---|---|---|
| internal/ | example.com/foo/internal/bar | Compiler rejects external importers |
| cmd/ | Thin main only | No business logic in main |
| Public API root | Stable packages at module root or pkg/ (team choice) | Document in README |
| No util catch-alls | Split by domain | Review nits on util package growth |
| Import cycles | Refactor via interfaces | go build failure |
| Blank imports | Side-effect registration only | Comment explains driver/plugin |
Security and supply chain
| Rule | Tool | Cadence |
|---|---|---|
| Vulnerability scan | govulncheck ./... | Every PR touching deps |
| Sum integrity | Commit go.sum | Never .gitignore go.sum |
| Private modules | GOPRIVATE, .netrc/token | Document in onboarding |
| SBOM export | Build pipeline artifact | Release tags |
| Pin CI linter/action versions | SHA or semver tags | Supply-chain policy |
go.work (monorepos)
| Rule | Do | Avoid |
|---|---|---|
| Local replace | go.work lists modules | Checked-in replace ../ hacks |
| CI builds each module | Matrix or go work sync | One module green, others broken |
| Publishable modules | Release from module dir without work-only replaces | Publishing with local paths |
Minimal module example
module example.com/widget
go 1.26
require (
golang.org/x/sync v0.10.0
)widget/
go.mod
widget.go // exported API
internal/
store/
store.go // not importable outside module
cmd/
widgetctl/
main.go // thin mainFAQs
When is a replace directive acceptable?
Temporary fork fixes with issue link and removal date.
Not a permanent substitute for upstream releases.
Should applications tag semver?
Optional.
Libraries publishing for go get must tag semver releases.
How do v2 modules work?
Import path includes /v2 suffix; major behavior changes ship there.
What if go mod tidy changes unrelated modules?
Run from module root; commit atomic tidy with the import that caused it.
Investigate if tidy churn is constant - often signals floating versions.
How minimal is minimal?
Every direct dependency needs an owner and removal plan.
Prefer stdlib and small focused modules over mega-frameworks for libraries.
Can I depend on internal company modules?
Yes with GOPRIVATE and documented versioning.
Treat them like external semver consumers.
When to use vendor/?
Air-gapped builds, reproducible deploys, or policy requiring checked-in sources.
Adds maintenance cost on upgrades.
How do retract directives help?
Mark bad tags so go get avoids them without deleting git history.
Should test-only deps appear in go.mod?
Yes in the main module file; use build tags to keep them out of production binaries.
Some teams split integration into separate modules.
What about pseudo-versions?
Use for pre-release commits; prefer tagged releases for production pins.
Document in Pseudo-versions, replace, and retract.
How do I audit transitive dependencies?
go mod graph, go mod why -m, and govulncheck reachable analysis.
Block merges on reachable critical CVEs without plan.
Does TinyGo change module rules?
Target and stdlib subset differ; keep module tidy but verify imports build on device toolchain.
Related
- Packages and Modules Basics - hands-on intro
- Package Naming and internal/ Directories - layout depth
- API Design Rules for Go Libraries - export semver
- Security Rules for Go Services - govulncheck and secrets
- Go Rules & Best Practices Summary - section index
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).