Packages and Modules: Go's Unit of Distribution
Go ships software as versioned modules made of packages.
That pairing replaced the old GOPATH layout and gives you reproducible builds, semver-aware upgrades, and a single import path story from your laptop to production.
Summary
- A package is the compile-time unit of code; a module is the versioned unit you publish, download, and pin in
go.mod. - Insight: Clear boundaries keep APIs small, prevent import cycles, and let the toolchain resolve dependencies with checksums instead of ad hoc vendoring.
- Key Concepts: import path, module path,
go.mod,go.sum, Minimal Version Selection (MVS),internal/, semantic import versioning. - When to Use: Starting a library, consuming open source, splitting a monorepo, or debugging why
go getpicked a particular version. - Limitations/Trade-offs: One module path maps to one major version line (
/v2suffix); fine-grained sharing still happens at package level, not per file. - Related Topics: Project layout, workspace mode, replace/retract directives, and the
gocommand build graph.
Foundations
Every .go file declares package name and belongs to exactly one package in one directory.
The import path is how other code refers to that package.
For code inside your module, the import path is your module path plus the directory path under the module root.
For example, module example.com/widget with file internal/api/handler.go is imported as example.com/widget/internal/api.
A module is a tree of packages rooted at a directory containing go.mod.
That file names the module (module example.com/widget), declares the minimum Go version, and lists required dependency versions.
Consumers record your module in their go.mod with a semver tag (or pseudo-version) and the toolchain downloads it into the module cache.
Packages are about compilation and visibility (exported identifiers, internal/ rules).
Modules are about distribution and versioning (tags, MVS, checksums).
Analogy: packages are rooms in a building; the module is the address and lease terms for the whole building.
Mechanics & Interactions
When you import "github.com/foo/bar/baz", the compiler loads package baz from module github.com/foo/bar at the version selected by MVS in your go.mod.
Semantic import versioning requires major version v2+ modules to include the major suffix in the module path (example.com/lib/v2), so breaking changes do not silently upgrade under the same import string.
The build list is the set of module versions the go command uses for a build.
MVS chooses the minimum version of each module that still satisfies every require in the graph.
That tends to pick conservative upgrades and keeps builds predictable across machines.
go.sum stores cryptographic hashes of module contents.
The toolchain verifies downloads against those lines so a compromised proxy cannot substitute code quietly.
// go.mod (module root)
module example.com/myapp
go 1.26
require (
github.com/google/uuid v1.6.0
golang.org/x/sync v0.10.0
)The go directive is a language/toolchain floor, not a runtime dependency.
Packages under internal/ may only be imported by code inside the parent tree, which lets you refactor private APIs without breaking external callers.
Advanced Considerations & Applications
Monorepos often host one module or many.
A single module keeps one go.mod and shared versioning; multiple modules let teams tag and release subsystems independently at the cost of more go.mod files and cross-module replace/workspace wiring.
Libraries published for wide reuse should keep module paths stable, tag releases, and document /v2 migration paths.
Application repos can use replace locally or go.work during development without publishing those overrides.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Single module | One version line, simple CI | Large graphs, coupled releases | Apps and small libraries |
| Multi-module monorepo | Independent tags per area | More go.mod maintenance | Platform teams, shared infra |
Workspace (go.work) | Local cross-module edits without replace | Not for published consumers | Active multi-module development |
| Vendor directory | Air-gapped or pinned CI | Manual refresh, repo bloat | Regulated or offline builds |
Publishing flows through version control tags that match go list -m -versions.
Pseudo-versions fill the gap when you depend on untagged commits.
Retract marks bad tags without deleting history, steering new resolves away from broken releases.
Common Misconceptions
- "Package name must equal the last segment of the import path." - Convention strongly encourages it, but
package mainand a few stdlib cases differ; the import path is authoritative for the compiler. - "go.mod pins the exact Go runtime my binary uses." - It pins the language/toolchain version used to build; the compiled binary is static with respect to the Go runtime you linked at build time.
- "I need GOPATH/src for my project." - Modules live anywhere on disk; GOPATH now mainly holds caches and legacy tooling paths.
- "Higher semver in go.mod always wins." - MVS picks the minimum version that satisfies all requirements, not the newest available tag.
- "internal/ is just a naming convention." - The compiler enforces it; imports from outside the allowed tree fail at compile time.
FAQs
What is the difference between a package and a module?
A package is compiled together from one or more .go files in a directory.
A module is the versioned unit defined by go.mod that contains those packages and declares dependencies.
Where does downloaded code live on disk?
Module sources and zips land in the module cache, typically under $GOPATH/pkg/mod.
Your project source stays in your repo; the cache is read-only from the toolchain's perspective.
Why do v2 modules need a /v2 in the import path?
Semantic import versioning keeps incompatible APIs on distinct import paths so existing code keeps compiling until it opts into /v2.
Do I commit go.sum?
Yes for applications and libraries.
It locks checksums so CI and teammates verify the same module content.
Can one directory contain multiple packages?
No - one package per directory (except test packages like package foo_test).
Split packages by subdirectory.
How does the toolchain pick a dependency version?
It builds the requirement graph from all go.mod files involved and applies Minimal Version Selection to choose the oldest semver that still satisfies every edge.
What is an import path versus a module path?
The module path is the prefix in go.mod.
The import path adds subdirectories and may include a major version suffix for v2+ modules.
When should I split a repo into multiple modules?
When subsystems ship on different cadences, have different consumer sets, or need isolated semver lines.
Stay with one module while boundaries are still fluid.
Does go get upgrade everything to latest?
go get -u bumps requirements toward newer versions, but normal builds still resolve via MVS and your recorded require lines.
Read the diff before merging.
Are packages from the standard library modules?
The standard library ships with the Go distribution and is not a separate downloadable module in normal builds.
It still uses the same package and import path rules.
Can consumers import my internal packages if they know the path?
No - the internal directory rule is enforced by the compiler for paths under your module root.
What happens if two modules require conflicting versions?
MVS raises the selected version to the minimum that satisfies both unless you add replace or exclude directives.
There is no diamond-inheritance linker like some other ecosystems.
Related
- Packages & Modules Basics - Runnable snippets for modules, imports, and layout
- go.mod, go.sum & Minimal Version Selection - Files and version resolution in depth
- Package Naming & internal/ Directories - Names and boundary enforcement
- Standard Project Layout for Services and Libraries - Where cmd, internal, and pkg go
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).