A module is a unit of versioning and distribution: a tree of Go packages
identified by a module path, with explicit dependencies and a recorded build
list. Modules replaced the older GOPATH workflow in Go 1.11+ and have been
the default since 1.16.
Anatomy of a module
my-module/
├── go.mod // declares module path, Go version, dependencies
├── go.sum // cryptographic hashes of dependencies (committed)
├── main.go
└── internal/...
A minimal go.mod:
module github.com/me/my-module
go 1.22
require (
github.com/google/uuid v1.6.0
golang.org/x/sync v0.7.0
)
The module path is what other code uses to import packages from your module:
import "github.com/me/my-module/internal/store"
Creating a module
mkdir my-module && cd my-module
go mod init github.com/me/my-module
The path doesn't have to exist on the network for local development, but if
you want others to go get it, the path must resolve to your repo via the
remote import path protocol (most commonly
GitHub: github.com/<user>/<repo>).
Adding a dependency
Just import it, then run:
go mod tidy
tidy resolves all imports it finds in the source, adds them to go.mod,
records hashes in go.sum, and removes any modules that are no longer used.
You can also pin versions explicitly:
go get github.com/google/uuid@v1.6.0
go get github.com/google/uuid@latest
Versioning rules (semver + MVS)
Modules use semantic versioning: vMAJOR.MINOR.PATCH.
- Versions
v0.x.yandv1.x.ylive at the module path as-is. - Versions
v2and above require the module path to end in/v2(or/v3, ...). This prevents accidental incompatible upgrades. - Go uses Minimum Version Selection (MVS): when multiple modules require different versions of the same dependency, Go picks the highest minimum. There is no SAT solver and no surprise upgrades.
replace and exclude directives
replace github.com/foo/bar => ../bar // local fork
replace github.com/foo/bar v1.2.3 => github.com/me/bar v1.2.4-fix
exclude github.com/foo/bar v1.5.0 // never select this version
Use replace for development; remove it before tagging a release.
internal/
Any package under a directory named internal/ is importable only by code
rooted at the parent of that internal/. It's the official way to mark
"private to this module".
github.com/me/m
└── internal
└── secret // importable from github.com/me/m/... only
Vendoring (optional)
go mod vendor
Copies all dependencies into a vendor/ directory and makes the build use
them. Useful for hermetic builds or air-gapped environments.
Workspaces (go work)
If you're developing several modules together (e.g. a service plus a shared library you also own), use a workspace:
go work init ./service ./shared-lib
This creates go.work, telling Go to use the local checkout of each module
listed even though they have separate go.mod files.
go.work is not committed in most projects (it's a per-developer
convenience), but you can commit it for monorepos.
go.sum and integrity
go.sum records h1:-prefixed checksums for every module + version your
build observes. The Go toolchain refuses a build if the sum doesn't match.
Commit go.sum.
The default checksum database (sum.golang.org) verifies modules; you can
disable or change it with GONOSUMCHECK / GOSUMDB.
Common workflows
go mod init <path> # new module
go get <pkg>@<ver> # add or upgrade a dependency
go get -u ./... # upgrade all dependencies (minor/patch)
go get -u=patch ./... # only patch upgrades
go mod tidy # sync go.mod / go.sum to imports
go mod why <pkg> # explain why a module is needed
go mod graph # dump dependency graph
go list -m all # show effective build list
go list -m -u all # show available updates
Publishing your module
- Push a Git repo at the module path.
- Tag a release:
git tag v1.0.0 && git push --tags. - Anyone can
go get github.com/you/yourmod@v1.0.0. - The Go module proxy (proxy.golang.org) caches it within minutes.
Run / inspect this repo's module
cd /Users/nelsonlai/sources/freelance/Go
cat go.mod
go list -m all
go test ./...