Packages, Modules & Project Layout Best Practices
Module hygiene, tagging, and publishing conventions.
These habits keep import graphs small, releases predictable, and monorepos maintainable as teams grow.
How to Use This List
- Apply during module creation, before tagging v1.0.0, and in release review.
- Automate with
go mod tidy,go test ./...,go vet ./..., andgolangci-lintin CI. - Treat breaking import path changes as semver events (
/v2for libraries). - Revisit when adding a second binary, splitting modules, or onboarding external consumers.
A - Module identity and versioning
- Choose a module path that matches the hosting domain. Use
github.com/org/repoor your real domain sogo getresolves forever. - Commit
go.modandgo.sumtogether. Reproducible builds and checksum verification depend on both files. - Run
go mod tidybefore every merge and release tag. Orphans and missing indirects should never surprise CI. - Tag releases with semver matching module major lines. v2+ modules need
/v2in the module path and import paths. - Retract bad tags instead of pretending they never existed. Document migration in release notes when retracting.
B - Dependencies and upgrades
- Pin direct deps with intentional
go get module@version. Avoid blind@lateston production main without changelog review. - Understand MVS before fighting the graph. Bump the minimum version that satisfies the graph; do not hand-edit transitive versions without reason.
- Use
go mod why -mto justify new transitive modules. Drop unused weight before it becomes entrenched. - Set
GOPRIVATEfor private modules and disable sumdb where appropriate. Document VCS auth for CI and laptops. - Prefer
go.workover machine-specificreplacepaths in shared branches. Local overrides belong in workspace files or documented dev setup.
C - Package design and layout
- Keep
cmd/thin; inject wiring inmain, logic elsewhere. Onemainpackage per binary undercmd/<name>/. - Put implementation detail in
internal/. Let the compiler enforce boundaries instead of README warnings alone. - Name packages for what they do, not
utilorcommon. Short lowercase names; avoid import stutter at call sites. - Export the smallest API surface that meets consumer needs. Uppercase identifiers are compatibility promises.
- Break import cycles with shared contract packages, not dot imports. Interfaces and DTOs live below both sides.
D - Publishing and consumption
- Document which packages are stable for external importers. README plus optional
pkg/or top-level library packages. - Add a LICENSE file compatible with your module proxy. Consumers and legal review expect it at the module root.
- Run
go test ./...andgo vet ./...on every tag candidate. Module consumers inherit your quality bar. - Publish changelog entries for API and module path changes. Include upgrade steps for
/v2migrations. - Vendor only with a documented refresh policy.
go mod vendorhelps air-gapped CI; do not let vendor rot silently.
E - Monorepo and multi-module repos
- Default to one module until release lines truly diverge. Extra
go.modfiles add operational cost. - Commit root
go.workwhen teams co-develop modules daily. DocumentGOWORK=offchecks for publish validation. - Tag each module independently in multi-module repos. CI should not assume one Git tag versions every submodule.
- Avoid cross-module
internal/assumptions.internaldoes not span module boundaries even in one Git repo. - Split modules when semver, ownership, or consumer sets differ. Not when folders merely look crowded.
FAQs
When should I create a v2 module path?
When you remove or change exported APIs incompatibly.
Ship example.com/lib/v2 and keep v1 on the old path until deprecation completes.
Should I commit replace directives?
Only for permanent forks with team agreement.
Temporary local paths belong in go.work or developer docs, not shared main branches.
How often should I tag library releases?
Tag when consumers need a reproducible upgrade point.
Patch tags for fixes, minor for backward-compatible features.
Is pkg/ required for open source libraries?
No - clarity matters more than folder name.
Document supported import paths in README regardless of layout.
What belongs in go.mod go directive?
The minimum Go toolchain your module requires for language features.
Raise it deliberately when adopting new stdlib or syntax.
Should apps use semantic import versioning?
Apps are main modules; breaking changes are internal.
Libraries owe semver and /v2 discipline to downstream importers.
How do I audit module bloat?
go list -m all, dependency scanners, and periodic go mod tidy after refactors.
Remove unused direct requires first.
When is vendoring worth it?
Air-gapped builds, disaster recovery, or strict third-party audit trails.
Pair with automated vendor refresh in CI.
Can I rename my module path?
Treat as breaking: new path, migration guide, and possibly major version bump.
Old path may need a compatibility module or README redirect.
What checks run before go publish?
Tests, vet, tidy diff clean, correct tag on commit, and LICENSE present.
Proxy ingestion is eventual; verify with go list -m -versions after tag push.
Related
- Packages and Modules: Go's Unit of Distribution - Conceptual overview
- go.mod, go.sum & Minimal Version Selection - Files and MVS
- Standard Project Layout for Services and Libraries - cmd, internal, pkg
- Packages & Modules Basics - Runnable starting points
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).