Manage dependencies

Manage Go dependencies using the go command for version control and govulncheck for security scanning.

The dependency problem

You write a small HTTP server. It works. You need a JSON parser, so you download a library. Two weeks later, a teammate clones the repository and runs the code. It crashes. The library author pushed a breaking change to the main branch, and your teammate pulled the latest version instead of the one you tested against. This happens in almost every language. Go solves it with a deterministic module system that treats dependencies like compiled artifacts, not live branches.

How Go tracks what you need

Go uses modules to lock your project to exact versions of every package it touches. A module is just a collection of packages shipped together with a version tag. The system relies on two files. go.mod records what you explicitly asked for. go.sum records the cryptographic hashes of every version actually downloaded, including transitive dependencies. The go command refuses to build if the hash on disk does not match the hash in go.sum. This prevents supply chain tampering and accidental version drift.

Think of go.mod as a recipe and go.sum as a sealed receipt. The recipe says you need two cups of flour. The receipt proves exactly which bag of flour arrived, down to the milligram and the serial number. If someone swaps the bag, the receipt does not match and the build stops.

Your first module

Start a new project by telling Go what your module path is. The path should match where the code will live, usually a domain you control.

// go.mod
module example.com/myproject

go 1.22

The module line sets the root import path for every package in this directory tree. The go line declares the minimum compiler version required to build the code. The tool will refuse to run on older versions.

Add a dependency by fetching it directly. The command resolves the version, downloads the source, and updates your manifest.

go get github.com/go-chi/chi/v5@latest

The @latest suffix tells the tool to find the highest semver tag that matches your Go version. If you omit the version, the tool defaults to the latest tagged release. After fetching, clean up the manifest to remove unused entries and lock indirect dependencies.

go mod tidy

This command compares your source code against go.mod. It adds missing direct dependencies, removes packages that are no longer imported, and recalculates the minimal set of indirect versions. Run it before every commit. The Go community treats go mod tidy as mandatory because it keeps the manifest honest.

What happens under the hood

When you run go get, the tool follows a strict resolution algorithm. It starts with your go.mod file and builds a dependency graph. For every package it encounters, it checks whether a version is already recorded. If not, it queries the remote repository for available tags. It then applies Minimal Version Selection. MVS picks the lowest version that satisfies all constraints in the graph. This prevents diamond dependency conflicts where two packages request different versions of a third package.

After selecting versions, the tool downloads the source archives and computes SHA-256 hashes. It writes the version and hash pairs to go.sum. The file is append-only by convention. You never edit it manually. If you delete it and run go mod tidy, the tool regenerates it from scratch.

The build process verifies every hash before compiling. If a remote repository changes a tag to point to different code, the hash mismatch triggers a hard failure. The compiler rejects the build with verifying module: checksum mismatch. This protects you from compromised repositories and accidental overwrites.

Real-world workflow

A production project rarely stops at go get. You will scan for vulnerabilities, control runtime behavior, and manage multiple modules. Here is a typical sequence for a web service.

First, add a router and a JSON package. Then verify the supply chain.

go get github.com/go-chi/chi/v5@latest
go get github.com/gorilla/schema@latest
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

The govulncheck tool analyzes your dependency graph against the Go vulnerability database. It reports CVEs, affected versions, and remediation steps. It runs locally and does not require network access during the scan phase.

Sometimes you need to toggle internal compiler or runtime behavior without changing code. The GODEBUG environment variable controls low-level switches like garbage collection pacing or TLS fallback behavior. You set it at runtime.

GODEBUG=gcpacer=off go run main.go

For source-level control, use //go:debug directives. These live at the top of a file and override GODEBUG for specific packages. They are useful when you want to enable a debug flag only for a single module in a larger workspace.

//go:debug gcpacer=off
//go:debug tls13=off

package main

// Main starts the HTTP server with custom runtime flags.
func Main() {
    // Server initialization happens here.
}

The //go:debug directive must appear before the package declaration. The tool parses it during compilation and injects the equivalent GODEBUG setting. This keeps runtime configuration close to the code that needs it.

When things go sideways

Module resolution fails in predictable ways. The most common error is a missing or mismatched hash. If you manually edit go.sum or clone a repository without the file, the build stops with missing go.sum entry for module providing package. Run go mod tidy to regenerate the missing entries.

Another frequent issue is version skew. You import github.com/pkg/errors but the module path in go.mod says github.com/pkg/errors v0.9.1. If your code imports a different version, the compiler rejects it with go.mod requires an older version of package. The fix is to align the import path with the version in go.mod or bump the version explicitly.

Direct and indirect dependencies also cause confusion. go.mod marks packages you import directly as direct dependencies. Everything else is indirect. The tool does not guarantee that indirect packages will receive security updates automatically. You must run go get -u or go get <package>@latest to bump them. The -u flag updates all dependencies to their latest minor or patch versions, respecting semver compatibility.

Vulnerability scanners sometimes report false positives. govulncheck flags code paths that exist in a dependency but are never executed in your application. The tool provides a --format flag to output machine-readable results. You can filter the output or suppress specific CVEs in your CI pipeline. Do not ignore the scanner. Review the report and update the dependency if a fix exists.

The //go:debug directive also has strict rules. It only works for known debug keys. If you type an invalid key, the compiler warns with //go:debug: unknown key. Check the GODEBUG documentation for your Go version before adding new directives. The list changes between releases.

Picking the right tool

Use go mod init when you start a new project and need a module root. Use go get when you want to add or bump a specific dependency to a known version. Use go mod tidy when you commit code and need to synchronize the manifest with your actual imports. Use govulncheck when you run CI checks and want to catch known vulnerabilities before deployment. Use GODEBUG when you need temporary runtime toggles for profiling or debugging without changing source files. Use //go:debug when you want to lock a debug flag to a specific package and keep it version-controlled. Use go work when you are developing multiple modules simultaneously and need local overrides. Use plain go build when you want to verify compilation without downloading new dependencies.

Where to go next