The missing piece in your build
You clone a repository from a teammate. You run go run main.go. The terminal immediately rejects you with a missing checksum error. You run go mod download. The error stays. You run go mod tidy. Everything compiles. You are left wondering why two commands that sound like they do the same thing behave so differently.
The confusion comes from mixing up three separate pieces of Go's module system. There is the manifest file that declares what your project needs. There is the cryptographic receipt that proves those dependencies haven't been tampered with. There is the local cache that stores the actual source code. go mod tidy edits the manifest and the receipt. go mod download only fills the cache. Understanding the boundary between editing files and populating storage is the difference between a predictable build and a fragile one.
How Go tracks what you actually need
Go treats your project as a module. A module is a collection of packages shipped together with a go.mod file. That file is the source of truth. It lists the module path, the Go version, and every dependency your code explicitly or implicitly requires. When you import a third-party library, Go resolves it through the module proxy, downloads the source, and records the exact version in go.mod.
The second file is go.sum. This file contains cryptographic hashes for every module and every file inside those modules. Go uses these hashes to verify that the code you are building matches exactly what the author published. If a single byte changes, the hash changes, and the build stops. This prevents supply chain attacks and accidental corruption.
The third piece is the module cache. On your machine, Go stores downloaded modules in a directory like $GOPATH/pkg/mod. This cache acts as a read-only pantry. When you build or run code, Go looks for dependencies here first. If they are missing, it fetches them from the proxy. If they are present but the checksums do not match go.sum, Go refuses to use them.
go mod tidy operates on the first two pieces. It scans your source code, builds a dependency graph, adds missing entries to go.mod, removes entries you no longer import, and recalculates every hash in go.sum. It changes your repository state.
go mod download operates on the third piece. It reads go.mod, checks the cache, fetches anything missing from the proxy, and stores it locally. It never touches go.mod or go.sum. It only prepares your machine to build.
Convention aside: always commit go.sum to version control. The community treats it as a required artifact, not a generated file. Skipping it breaks reproducible builds and defeats the checksum verification system.
The minimal difference
Create a fresh directory and initialize a module. Add a single import from a well-known library. Run the two commands side by side to see how they diverge.
mkdir demo-modules && cd demo-modules
go mod init example.com/demo
# Initialize the module file with a basic path and Go version.
# Add a dependency manually to simulate a real project.
echo 'package main' > main.go
echo 'import "github.com/go-yaml/yaml/v3"' >> main.go
echo 'func main() {}' >> main.go
# Create a minimal entry point that pulls in an external package.
go mod tidy
# Scans imports, resolves versions, writes go.mod and go.sum.
go mod download
# Reads go.mod, fetches sources to the local cache, leaves files alone.
Run go mod tidy first. The toolchain reads main.go, sees the yaml import, contacts the module proxy, resolves the latest compatible version, writes it to go.mod, and generates the corresponding hashes in go.sum. Your repository now has two new files.
Run go mod download next. The toolchain reads the updated go.mod, checks your local cache, finds nothing, downloads the yaml module and its transitive dependencies, and stores them in $GOPATH/pkg/mod. Your go.mod and go.sum remain unchanged. The cache is now populated.
Run go build .. Go verifies the cached sources against go.sum, compiles the packages, and produces a binary. The build succeeds because the manifest, the receipt, and the cache all agree.
Convention aside: go.mod is the only file you should edit by hand for version bumps or replace directives. Let the toolchain manage go.sum. Manual edits to the checksum file almost always break verification.
What happens under the hood
The Go toolchain follows a strict sequence when resolving modules. It starts with your go.mod file. It parses the require block and builds a directed graph of direct and indirect dependencies. Direct dependencies are packages you import. Indirect dependencies are packages those imports require. Go records both, but marks indirect ones with a comment in go.mod so you know they are not your responsibility.
Next, the toolchain contacts the module proxy. The default proxy is proxy.golang.org, which mirrors public repositories and strips out .git directories and build artifacts. The proxy returns a minimal zip file containing only the Go source, go.mod, and license files. This keeps downloads fast and reduces attack surface.
Once the zip arrives, Go extracts it to a temporary directory. It computes SHA-256 hashes for every file and for the module as a whole. It compares those hashes against go.sum. If they match, Go moves the module into the read-only cache. If they do not match, Go aborts and prints a verification error.
go mod tidy repeats this resolution process but with a write phase. It walks your entire package tree, collects every import path, resolves versions using semantic import versioning, and updates go.mod. It then recalculates go.sum from scratch. If you removed an import, tidy drops the dependency from the manifest. If you added one, tidy adds it. The command is idempotent. Running it twice produces the same result.
go mod download skips the resolution and write phases. It trusts go.mod as written. It only ensures the cache contains the exact versions listed. This makes it safe to run in environments where you cannot or should not modify source files.
Convention aside: the module proxy respects GONOSUMCHECK and GONOSUMDB environment variables. Internal or private modules often bypass the public checksum database. Configure these variables in CI if your organization hosts its own proxy.
Real world: CI pipelines and cache warming
Continuous integration systems benefit from separating file synchronization from cache population. A typical pipeline runs on a fresh container. The first step checks out the code. The second step ensures the module files match the actual imports. The third step downloads everything to the cache. The fourth step runs tests or builds.
# Step 1: Synchronize the manifest with the codebase.
go mod tidy
# Fails fast if imports drift from the declared versions.
# Step 2: Populate the local cache without touching files.
go mod download
# Fetches sources to the read-only cache for faster compilation.
# Step 3: Run the test suite.
go test ./...
# Uses the cached modules and verifies checksums automatically.
This pattern prevents accidental file changes from leaking into your build artifacts. If go mod tidy modifies go.mod or go.sum, the pipeline can detect uncommitted changes and fail the job. That failure tells you a developer forgot to run the command locally.
Docker multi-stage builds use the same separation. The first stage downloads dependencies and caches them. The second stage copies the cache and compiles the final binary. This keeps the production image small while preserving build speed.
# Stage 1: Download and cache dependencies.
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
# Copy only the manifest files to leverage Docker layer caching.
RUN go mod download
# Fetch modules to the Go cache before copying source code.
COPY . .
# Copy the rest of the application after dependencies are cached.
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# Compile the binary using the pre-fetched modules.
# Stage 2: Minimal runtime image.
FROM alpine:3.19
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# Ship only the compiled binary to reduce attack surface.
Convention aside: go mod download is the standard command for Docker layer caching. It ensures the dependency layer only rebuilds when go.mod or go.sum changes, not when you edit a single .go file.
When things go sideways
The module system is strict by design. That strictness surfaces errors early, but it also creates friction when the cache, the manifest, and the proxy disagree.
The most common error appears when go.sum is missing or outdated. The compiler rejects the build with go: missing go.sum entry for module providing package github.com/example/pkg. This happens when you copy go.mod but forget go.sum, or when you manually edit the manifest without running tidy. The fix is to run go mod tidy and commit the updated receipt.
Another frequent issue is cache corruption. If a network interrupt leaves a partial download in $GOPATH/pkg/mod, Go may refuse to use it. The toolchain prints go: downloading github.com/example/pkg v1.2.3: verifying module: checksum mismatch. Clearing the cache with go clean -modcache forces a fresh download. Use this sparingly. It slows down subsequent builds while the proxy refetches everything.
Private modules introduce their own complications. If your go.mod references an internal repository, the public proxy cannot resolve it. You must configure GOPRIVATE or set up a corporate proxy. Without it, go mod download fails with go: github.com/internal/corp/pkg: module lookup disabled by GOPROXY=off. The solution is to align your environment variables with your organization's module hosting strategy.
Convention aside: never ignore go.sum in .gitignore. The community treats it as a required artifact. Skipping it breaks reproducible builds and defeats the checksum verification system.
Which command to run
Use go mod tidy when you add or remove imports and need the manifest to match your actual code. Use go mod tidy when you want to clean up unused dependencies after refactoring. Use go mod tidy when you are preparing a pull request and want to guarantee the repository state is consistent.
Use go mod download when you are warming a cache in a CI pipeline or Docker build. Use go mod download when you need to fetch dependencies without risking accidental changes to go.mod or go.sum. Use go mod download when you are troubleshooting a build and want to isolate cache population from file synchronization.
Use go get when you intentionally want to add or upgrade a specific dependency to a newer version. Use plain go build or go run when you trust the existing manifest and cache and just want to compile or execute.
The module system separates declaration from storage. Keep that boundary clear.