How to Remove Unused Dependencies with go mod tidy

Cli
Run `go mod tidy` in your module root to automatically remove unused dependencies from `go.mod` and `go.sum`, while adding any missing ones required by your code.

The drifting manifest problem

You just finished refactoring a module. You removed three imports, deleted the related functions, and ran your tests. Everything passes. You commit the code, push it, and the CI pipeline fails. The build log complains about missing checksums. The problem is not your code. It is your go.mod and go.sum files. They still list packages you no longer use, and they are missing the exact versions your remaining code actually needs.

Go modules track dependencies through two files. go.mod lists the direct packages your code imports and the minimum versions required. go.sum stores cryptographic hashes for every single dependency, including the ones your dependencies rely on. Think of go.mod as a recipe card and go.sum as a sealed ingredient checklist. If you change the recipe but forget to update the checklist, the kitchen cannot verify what you actually have. go mod tidy is the tool that cross-references your source code against both files, removes dead weight, and fills in the gaps.

A minimal cleanup

Here is the simplest way to trigger the cleanup. Create a fresh module, add an import, then remove it and run the command.

// main.go
package main

import (
	"fmt"
	// This import will be stripped because it is not used in main()
	"github.com/example/unused-lib"
)

func main() {
	// Only fmt is actually called here
	fmt.Println("hello")
}
# Initialize the module if it does not exist
go mod init example.com/myapp

# Add the library to go.mod manually to simulate drift
go get github.com/example/unused-lib@v1.0.0

# Run the cleanup command
go mod tidy

The command scans every .go file in the module. It finds that unused-lib is no longer imported anywhere. It strips the entry from go.mod and removes the corresponding hash from go.sum. If you had added a new import instead, the command would have fetched the latest compatible version and written it to the manifest files.

gofmt does not format go.mod. The module command handles its own indentation and ordering. Run go mod tidy to keep the manifest clean.

The resolution algorithm

The tool does not guess. It follows a strict resolution process. First, it parses your source tree and builds a list of every import path. Next, it compares that list against the require block in go.mod. Any package in the manifest that does not appear in your code gets marked for deletion. Any package in your code that is missing from the manifest gets resolved against the Go module proxy or your configured mirrors.

Go uses Minimum Version Selection. If your code imports github.com/example/lib@v1.2.0 and that library depends on github.com/other/helper@v2.1.0, the resolver picks the highest version required by any direct or indirect dependency. It never picks a lower version just because one package is satisfied with it. This prevents diamond dependency conflicts where two packages expect different versions of the same third package.

Indirect dependencies work the same way. If github.com/example/used-lib depends on github.com/other/helper@v2.1.0, the tidy command ensures helper appears in go.mod with the // indirect comment. That comment tells other developers and tools that your code does not import the package directly, but it is required for the build to succeed. The tool also recalculates the cryptographic hashes in go.sum to match the exact versions now in play.

Running the command twice produces identical results. The operation is idempotent. You can run it after every commit, before every merge, or as a pre-commit hook, and the manifest files will settle into a stable state. The convention in the Go community is to run it locally before pushing and enforce it in CI. Trust the tool to keep the graph consistent.

Enforcing consistency in CI

Real projects rarely stay static. You pull a branch, merge main, and suddenly your local go.sum diverges from the repository. Or a dependency releases a patch version that changes an indirect requirement. Here is how a typical CI pipeline catches drift before it reaches production.

# Reset manifests to match the repository state exactly
git checkout -- go.mod go.sum

# Run the dependency resolver against the current code
go mod tidy

# Fail the pipeline if the manifests changed unexpectedly
git diff --exit-code go.mod go.sum

The script resets the manifest files to match the repository, runs the resolver, and checks the exit code. If git diff returns zero, the files are perfectly synchronized. If it returns non-zero, the pipeline fails and alerts the developer that the code and the manifests are out of sync. This pattern prevents accidental version bumps and catches forgotten imports before they break downstream builds.

Many teams add a go build ./... step after the tidy check. The resolver only validates imports and versions. It does not compile your code. Running the compiler ensures the resolved versions actually work together. Keep the dependency graph and the build step separate.

When the tool stops

The command will not run if your code refuses to parse. A missing closing brace or a misplaced semicolon stops the scanner. The toolchain responds with expected '}', found 'EOF' or a similar syntax error. Fix the syntax first. The resolver cannot analyze broken code.

Network issues also halt the process. If the Go module proxy is unreachable or a private repository requires authentication that is not configured, the command fails with module requires Go 1.21 or verification: no checksums recorded. Configure your GOPRIVATE environment variable for internal packages and ensure your network allows outbound HTTPS traffic to proxy.golang.org.

Typos in import paths are another common trap. The tool will attempt to fetch github.com/example/typo-lib and fail when the repository does not exist. The error reads module github.com/example/typo-lib: not found. Double-check the path against the actual repository URL. Go modules use the repository path as the module path by default, so a mismatched URL breaks the resolution chain.

The resolver also respects the Go version declared in your go.mod file. If you request a package that requires a newer language version than your module specifies, the command stops with go.mod requires go 1.22, but go.mod in calling module declares go 1.20. Bump the version line in go.mod to match the requirement. The tool enforces version compatibility strictly.

Replace directives complicate the picture. If you use replace github.com/example/lib => ../local-lib to test a fork, go mod tidy will still process the rest of the graph, but it will leave the replace directive intact. The tool does not remove local overrides automatically. You must delete them manually when you are ready to ship. The resolver follows your explicit instructions.

Choosing the right command

Use go mod tidy when you need to synchronize your manifest files with the actual imports in your source tree. Use go get when you want to intentionally add a new dependency or upgrade a specific package to a newer version. Use go mod vendor when you need to copy all dependencies into a local vendor directory for offline builds or strict isolation. Use manual editing of go.mod only when you need to pin a specific version override or adjust the Go version line. Let the tool handle the graph.

Where to go next