Modules, dependencies, and the go command
How Go finds your code and other people’s, and the commands worth memorising.
- Create a module with
go mod initand explain what go.mod and go.sum each record. - Explain why Minimal Version Selection picks the oldest version that satisfies every requirement.
- Run
go vet,gofmtandgo test -racethe way CI would.
What a module is, and what the go command does
A module is one project: a folder with a go.mod file at the top, containing one or more packages. go.mod records the module's name, the Go version it needs, and every outside package it depends on.
A dependency is a package written by someone else that your code imports. The import path starts with where the code lives, such as github.com/jackc/pgx. By default the go command downloads it through the Go module mirror (proxy.golang.org), which fetches from that repository. There is no registry you have to publish to.
The go command is the single tool that does everything: builds, tests, formats, fetches dependencies, and reads documentation. This topic covers the parts of it you will use weekly, and the four checks worth running before every push.
go.mod and go.sum
module github.com/you/snapnotesgo 1.25require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/jackc/pgx/v5 v5.7.5 golang.org/x/crypto v0.43.0)require ( github.com/jackc/puddle/v2 v2.2.2 // indirect)- `go.mod` declares the module path, the minimum Go version, and your dependencies.
- `go.sum` holds cryptographic checksums of every module version you use. If a download ever differs from what was recorded, the build fails. Commit it.
- The `go` line matters. It gates language behaviour: a module declaring
go 1.21keeps the old per-loop variable semantics even on a newer toolchain.
Minimal Version Selection
Many package managers resolve to the *newest* version that satisfies everyone. Go picks the oldest version that satisfies everyone: the highest of the minimums that your modules ask for. That sounds backwards until you notice the consequence: your build does not change because someone else published a release. Upgrades only happen when you ask for one.
This is why Go needs no lockfile. go.mod plus MVS is already deterministic.
The commands
go mod init github.com/you/proj # start a modulego get example.com/pkg@latest # add or upgradego get example.com/pkg@v1.2.3 # pin an exact versiongo get example.com/pkg@none # removego mod tidy # add what is missing, drop what is unusedgo mod why -m example.com/pkg # why is this in my build?go mod graph # the full dependency graphgo mod verify # do the downloaded modules match go.sum?go mod vendor # copy deps into ./vendor (for air-gapped builds)go build ./... # compile everythinggo run ./cmd/api # compile and rungo test ./... # test everythinggo vet ./... # find real bugsgo doc net/http.ServeMux # read the docs offlinego clean -modcache # nuclear option when a download is corruptWorkspaces, for working on two modules at once
go work init ./api ./sharedlibgo work use ./anothermodule# now ./api's import of sharedlib resolves to your LOCAL copy,# with no replace directive polluting go.modBuild flags worth knowing
CGO_ENABLED=0 go build -ldflags="-s -w" -o api ./cmd/api # static, strippedgo build -ldflags="-X main.version=$(git rev-parse --short HEAD)" ./cmd/apiGOOS=linux GOARCH=arm64 go build ./cmd/api # cross-compile, no toolchain neededgo tool dist list # every supported targetgo build -race ./... # binary with race checks built ingo build -gcflags='-m' ./... 2>&1 | head # what escaped to the heapThe tools that belong in CI
- run: go build ./...- run: test -z "$(gofmt -l .)" # fails if anything is unformatted- run: go vet ./...- run: go test -race -coverprofile=c.out ./...- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...- `gofmt`: not a preference. With the CI line above, unformatted code fails the build.
- `go vet`: ships with Go, and its checks are chosen to have very few false positives. Run it always.
- `govulncheck`: from the Go team, and low-noise because it reports vulnerabilities only in functions your code can reach.
- `staticcheck`: a widely used third-party linter that finds real bugs vet misses. Add it once
vetis clean. - `golangci-lint`: a runner for many linters. Enable a small set, not sixty, or you will spend your time writing
//nolint.
Publishing a package
- The module path is the repository URL:
github.com/you/thing. - Tag a release with a semver tag:
git tag v1.0.0 && git push --tags. That is the whole publishing process. There is no registry to upload to. - Breaking change? The major version goes in the path:
github.com/you/thing/v2. That way v1 and v2 can coexist in one build. - Before v1.0.0 you are allowed to break things. After it, a breaking change needs a new major version.
- Commit
go.modandgo.sum. Minimal Version Selection makes builds reproducible with no lockfile. go mod tidybefore every commit;go workfor developing two modules together.CGO_ENABLED=0plusGOOS/GOARCHcross-compiles to anything with no toolchain.- CI runs gofmt, vet, test -race, and govulncheck. That is the whole gate.
go mod why on each of your dependencies and find out what actually pulls each one in. Then run go mod tidy and check whether anything disappears.- 1Run
go mod why -mon each dependency and find out what actually pulls each one in. - 2Run
go mod tidyand check whether anything disappears. - 3Set up a
go.worklinking two local modules and edit one from the other with noreplacedirective. - 4Add the four CI commands to a Makefile target called
checkand run it before your next push.