AlgoMaster Logo

Go Modules (go.mod, go.sum)

Last Updated: May 22, 2026

High Priority
10 min read

A Go module is a collection of related packages versioned and distributed together as a single unit. Every module has a go.mod file at its root that declares the module's identity, its Go version, and the third-party libraries it depends on. This file plus its companion go.sum turns a folder of .go files into a reproducible, shareable project that anyone can clone, run, and build with confidence.

Module vs Package

A package is one directory of .go files that share the same package declaration. A module is a tree of packages rooted at a directory that contains a go.mod file. One module can hold one package or hundreds, and the go.mod is the only thing that declares "everything under this folder is part of one versioned unit".

Here's a tiny store module with three packages:

The folder github.com/store-co/cart is the module. The packages main, catalog, and pricing live inside it. They're released together, versioned together, and tagged with the same git tag. A consumer who imports github.com/store-co/cart/pricing at version v1.2.0 gets a snapshot of the whole tree at that tag, not just one folder.

The diagram shows the layering. One go.mod defines the module. The module contains three packages. Each package exports identifiers (capitalized names like Product or ApplyTax) that other packages can use. When you ship a release of this module, you ship the whole tree as one versioned blob.

From GOPATH to Modules

Before Go 1.11, Go code lived under a single $GOPATH/src directory. Every project, including your own, had to sit inside that tree, and there was no built-in way to pin third-party dependencies to a specific version. Tools like dep and godep filled the gap, but they were external.

Modules arrived in Go 1.11 as an experiment, became the default in Go 1.13, and were made mandatory in Go 1.16. Today, every Go project you create starts with go mod init. The GOPATH directory still exists, but only as a download cache and build target. Your source code can live anywhere.

You no longer have one giant workspace. Each module is a self-contained folder that knows its own identity (module line), its dependencies (require block), and the exact versions it builds against (go.sum).

Creating a Module with go mod init

The command go mod init creates a new module by writing a go.mod file. You pass it the module path, which is the canonical name other code will use to import this module:

The result:

And the file go.mod now exists:

That's it. Two directives. The module line says "this folder, and everything under it, is the module github.com/store-co/cart". The go line records the Go language version this module is written for.

The module path matters because it doubles as the import path prefix for every package in the module. If you create a folder pricing/ inside this module, code in other modules imports it as github.com/store-co/cart/pricing. The folder name is the suffix; the module path is the prefix; together they form the canonical import path.

By convention, the module path looks like a URL you could clone with git. github.com/store-co/cart says "you can fetch this module from the GitHub repo at that URL". Go tooling actually does this: when someone runs go get github.com/store-co/cart, the toolchain treats the path as a URL, hits the GitHub API, and downloads the matching tag. Picking a path you don't own (like github.com/google/cart for a personal project) works for local development but breaks the moment anyone else tries to use the module. Pick a path you control.

Internal modules at a company often use a private domain prefix like example.com/shop/catalog or a non-VCS path that's never published. The same rules apply: the path is the identity, and it's permanent.

The go.mod File in Depth

A fresh go.mod is small. A real one grows as you add dependencies. Here's a realistic example for a small store backend that uses two third-party libraries:

Five directives matter:

`module` declares the module path. There's exactly one module line per file. Change it and the import paths for every package in the module change.

`go` records the Go language version this module is written against. Since Go 1.21, this line is enforced: the toolchain refuses to build the module with an older Go version. It also controls which language features (generics, range-over-func, and others) are available. Set it to the lowest version of Go you actually need.

`require` lists the modules this one depends on, each pinned to a specific version. The two require blocks above show the conventional split: direct dependencies (code your own packages import) in one block, indirect dependencies in another. You can have one block or many; the split is just for readability.

`// indirect` is a comment, but it's load-bearing. The Go toolchain writes it next to any required module that your code doesn't import directly. Indirect deps come in two flavors. Either your direct deps require them (transitive), or they used to be imported and you've cleaned up but haven't run go mod tidy yet. After go mod tidy, every // indirect entry is a true transitive dependency.

In the example above, your code imports github.com/fatih/color and github.com/google/uuid directly. color itself depends on go-colorable, go-isatty, and golang.org/x/sys. Those three show up as indirect because the Go toolchain needs to know which exact versions to use, even though your own code never imports them by name.

Two more directives show up less often but matter:

`replace` swaps out a dependency for a different source. Two common uses:

The first form points at a local directory, useful when you're developing two related modules side by side without publishing intermediate releases. The second form swaps a published module for a fork, which is how teams patch a bug they can't get merged upstream.

`exclude` blocks a specific version of a dependency:

The minimum version selection algorithm picks higher versions when this is set. Used rarely, typically when a specific release is known broken.

There's also a `retract` directive that lets the module's own author withdraw a published version (so consumers know to avoid it). That belongs to the publishing flow.

The format is human-editable. You can open go.mod in your editor and add a replace line by hand. Most of the time, though, tools like go get and go mod tidy edit it for you.

The go.sum File

While go.mod says which versions to use, go.sum says what those versions are supposed to look like. It's a checksum database. Every time the toolchain downloads a module, it computes a hash and compares against the entry in go.sum. If the hashes don't match, the build fails immediately.

A go.sum snippet for the example above:

Each module has two lines per version:

  • The first (with no /go.mod suffix) is the hash of the full module zip file (all the source code).
  • The second (with the /go.mod suffix) is the hash of just that version's go.mod file.

Both checks matter. The full-zip hash catches tampering with the source. The go.mod-only hash catches tampering with the dependency list before any code has been downloaded. The format h1: is the hash algorithm version; h1 is SHA-256.

Commit go.sum to git. Without it, two people building the same module at the same version could end up with subtly different code, either because someone overwrote a tag in a repo or because the proxy served different bytes. With go.sum in version control, every build is verified against the same expected hashes. If a hash doesn't match, the toolchain refuses to proceed:

That's the safety net. A compromised proxy, a force-pushed tag, or a corrupted download all surface as the same loud error. The Go module proxy (proxy.golang.org) also maintains a global checksum database (sum.golang.org) that records hashes the first time anyone fetches a module version, so even if your local go.sum is missing an entry, the toolchain can still verify against the public log.

go mod tidy

This is the most important command in the module workflow. It walks your code, figures out which packages you actually import, and reconciles go.mod and go.sum against reality. Two things happen:

  1. Any imported package whose module isn't in go.mod gets added.
  2. Any required module that no code imports anymore gets removed.

Picture the lifecycle. You add a new import:

Running go run main.go errors out because github.com/google/uuid isn't in go.mod. You run:

The toolchain finds the import, looks up the latest version of github.com/google/uuid, writes it into the require block of go.mod, downloads the module, writes its hashes into go.sum, and exits silently. Now go run works.

Later, you delete the uuid import. go.mod still says you require it. Run go mod tidy again, and the require line is removed, along with its go.sum entries.

The diagram shows what go mod tidy touches. It reads your source code's import graph, then writes the three things it manages: the require list in go.mod, the checksum entries in go.sum, and the downloaded modules in the module cache. You usually don't touch any of these by hand.

A typical workflow:

Run go mod tidy before every commit that touches imports. It's the cleanup pass that keeps go.mod honest.

Other Module Commands

A few commands round out the day-to-day toolkit.

`go mod download` fetches modules to the local cache without changing go.mod. Useful in CI pipelines, where you want to populate the cache as a separate step before running the actual build:

It also helps when a teammate cloned the repo and wants to grab dependencies up front for offline work.

`go mod why <package>` explains why a particular package is in your dependency graph. Pass it an import path, and it traces the shortest chain from your module to that package:

Output:

Read it bottom up: go-isatty is here because color requires it, and color is here because your module requires it. When a strange indirect dep shows up and you can't figure out who pulled it in, go mod why is the answer.

`go mod graph` prints the entire dependency graph in plain text. One line per edge, in the format module@version dependency@version:

Sample output:

This is the raw data behind go mod why. Pipe it to grep or sort to investigate large graphs.

A small reference table of common commands:

CommandWhat it does
go mod init <path>Create a new go.mod with the given module path
go mod tidySync go.mod and go.sum with the imports your code actually uses
go mod downloadFetch all required modules into the local cache
go mod why <pkg>Show the chain of requires that brought <pkg> in
go mod graphPrint the full dependency graph
go mod verifyCheck that cached modules still match their go.sum hashes
go get <pkg>@<ver>Add or upgrade a dependency to a specific version

go get deserves a brief mention. It adds, upgrades, or downgrades a dependency in go.mod. go get github.com/google/uuid@v1.6.0 pins to that exact version. go get github.com/google/uuid@latest jumps to the newest published release. After go get, you still typically run go mod tidy to clean up any indirect entries that shifted.

The Module Cache

When Go downloads a dependency, it doesn't dump it into your project folder. It puts it in a global cache, shared across every Go project on your machine. The default location is $GOPATH/pkg/mod, which on most systems means:

Each module/version combination lives in its own directory. Two different projects that both require github.com/google/uuid@v1.6.0 share the same on-disk copy. Two projects that need different versions get two different directories side by side.

The cache is read-only by design. The files have their write permission bits cleared, so even root can't modify them without explicitly changing permissions. This isn't paranoia; it's correctness. If go.sum says a module's hash is h1:NIvaJDM..., the toolchain has to know those bytes haven't drifted since the last verify. Read-only files make accidental edits impossible, and they short-circuit any tool that tries to "patch" a dependency in place.

If you want to experiment with a dependency, copy it into your own module and use a replace directive to point at the copy. Don't edit the cache.

To clean the cache:

This wipes everything. The next build will re-download what it needs. You rarely need this; the cache is content-addressed and self-correcting, so leftover modules don't really hurt. The main reason to clean is to reclaim disk space after years of adding and removing dependencies across many projects.

Putting It Together

A normal lifecycle for a new module looks like this:

The code:

After the first go mod tidy, your go.mod is:

And go.sum has two lines for uuid, the zip hash and the go.mod hash. Commit both files to git. Anyone who clones the repo can run go build and get a byte-for-byte identical build because every dependency, every transitive dependency, and every checksum is pinned.