When dependencies break the build
You write a small HTTP service. You import a popular router package. The code compiles. You push the changes to a repository. The CI runner fails because the router library released a breaking change overnight. Or you clone the repo on a teammate's laptop, run go run, and get a wall of errors about missing packages. Before Go 1.11, this was the daily struggle of GOPATH hell. Modules fix this by making your project the source of truth for its dependencies.
A module is a collection of Go packages shipped together. The go.mod file is the manifest. It lists your module's path and the versions of other modules you need. The go.sum file is the checksum ledger. It records the cryptographic hash of every downloaded file. If someone tampers with a dependency or a version changes on the server, the hash won't match, and the build stops. This ensures that go build produces the exact same result on every machine, every time.
Commit both files to version control. The go.mod defines the requirements. The go.sum proves the integrity. Without go.sum, the build cannot verify dependencies, and you lose reproducibility.
Initializing and fetching
Start by creating the module manifest. The module path should be a unique identifier, usually a URL pointing to a version control repository. It does not have to exist yet, but it must be unique to avoid collisions.
# Create the module manifest with a unique path
go mod init example.com/myapp
This command generates go.mod. The file contains the module path and the Go version your project requires. When you import a third-party package, the tool fetches it automatically.
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux" // Third-party router for flexible routing
)
// main initializes the router and starts the server.
func main() {
r := mux.NewRouter()
// Define a handler that writes a response to the client
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello from mux"))
})
fmt.Println("Listening on :8080")
http.ListenAndServe(":8080", r)
}
Run the program. The tool sees the import of github.com/gorilla/mux. It checks the module cache. If the package is missing, it queries the module proxy, downloads the code, and updates go.mod and go.sum.
# Run the program; go automatically downloads dependencies if missing
go run .
The go.mod file now includes a require directive for the router. The version recorded is the minimum version required to build your code. This is the core of Go's dependency resolution: the tool picks the lowest version that satisfies all constraints. This minimizes the blast radius of updates and keeps your dependency tree stable.
How version resolution works
Go modules use semantic versioning. Versions follow the vMAJOR.MINOR.PATCH format. For major version one, minor versions can add features, but they must not break the API. Patch versions contain only bug fixes.
When you run go get github.com/gorilla/mux, the tool selects the latest minor version available. If v1.8.1 is the latest, that's what gets added. If you later import a package that requires v1.7.0, the tool keeps v1.8.1 because it satisfies both requirements. If another package requires v1.9.0, the tool bumps the version to v1.9.0.
This algorithm is called Minimal Version Selection. It ensures that every dependency gets the lowest version that works for the entire project. You rarely need to specify exact versions. The tool handles the math.
If you need to force a specific version, you can use the @ syntax. This is useful when a newer version introduces a regression you want to avoid.
# Pin to a specific version
go get github.com/gorilla/mux@v1.8.0
The tool updates go.mod to require exactly v1.8.0. Future runs of go mod tidy will not upgrade this dependency unless you explicitly ask for it.
Major versions and import paths
Major version two introduces breaking changes. Go modules handle this by requiring a change in the import path. A module at v2.0.0 or higher must include the major version in its path.
If github.com/gorilla/mux releases v2.0.0, the import path becomes github.com/gorilla/mux/v2. This design prevents accidental mixing of major versions. Your code can import v1 and v2 side by side if needed, and the tool keeps them separate.
import (
// v1 path
"github.com/gorilla/mux"
// v2 path requires the /v2 suffix
"github.com/gorilla/mux/v2"
)
The module author must update the module path in their go.mod file to match. If they release v2 without updating the path, the tool rejects the version. This rule enforces semantic versioning at the tooling level.
Cleaning up with go mod tidy
As you develop, you add and remove imports. The go.mod file can drift. Unused dependencies might linger. Indirect dependencies might be pinned to versions higher than necessary.
Run go mod tidy to clean up the manifest. This command analyzes your source code. It adds missing dependencies. It removes unused ones. It updates indirect dependencies to the minimum versions required by your direct dependencies. The result is a minimal, reproducible go.mod.
# Clean up the manifest and lock indirect dependencies
go mod tidy
Always run go mod tidy before committing. It ensures that go.mod matches your code. It also refreshes go.sum with the correct checksums. If you skip this step, you might commit a manifest that requires packages you no longer use, or miss a dependency that your code actually needs.
Pitfalls and compiler errors
If you run go build in a directory without go.mod, the compiler rejects the program with go.mod file not found in current directory or any parent directory. You must initialize the module first. Modules replace GOPATH for dependency resolution. Your project can live anywhere on the file system.
If you manually edit go.mod to a version that does not exist, the tool complains with module ...: reading ... go.mod: unknown revision. Check the version number and the module path. Typos in the path are a common cause.
If the go.sum file gets out of sync, perhaps because you copied go.mod but not go.sum, the build fails with verifying module checksum: checksum mismatch. The tool refuses to trust the dependency. Run go mod download to refresh the checksums. Never edit go.sum by hand. The tool manages it.
Private repositories require extra configuration. The module proxy cannot access private code. Set the GOPRIVATE environment variable to tell the tool to skip the proxy for specific paths.
# Configure the tool to skip proxy for internal paths
go env -w GOPRIVATE="example.com/internal/*"
This convention uses a glob pattern. The tool treats matching modules as private and fetches them directly from the version control system. You still need authentication configured for your VCS client.
Decision: managing dependencies
Use go mod init when starting a new project to create the module manifest. Use go get when adding a new dependency or updating a specific package to a newer version. Use go mod tidy after changing imports to remove unused dependencies and lock indirect versions. Use go mod vendor when you need to bundle dependencies inside the repository for environments with no network access. Use the replace directive when testing a local fork of a dependency or pointing to a private repository that the proxy cannot reach. Use go work when managing multiple related modules in a monorepo setup.
Modules make your project portable. The manifest travels with the code. Trust the checksum. If go.sum complains, something changed. Run go mod tidy before every commit. Keep the manifest minimal.