Free Ebook cover Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

New course

12 pages

Go Modules: Dependencies, Versioning, and Reproducible Builds

Capítulo 4

Estimated reading time: 9 minutes

+ Exercise

1) Create a module with go mod init (and module path conventions)

A Go module is the unit of versioning and dependency management in modern Go. A module is defined by a go.mod file at the root of your project. Once you use modules, your project’s dependencies are resolved by module versions rather than by GOPATH folder layout.

Step-by-step: initialize a new module

Create a new folder and initialize a module:

mkdir moddemo
cd moddemo
go mod init example.com/moddemo

This creates a go.mod file. The module path (here example.com/moddemo) is an identifier used in import paths for packages inside your module.

Module path conventions

  • Use a domain you control for public modules (for example, github.com/you/project). This makes the path globally unique.
  • For local learning projects, you can use example.com/... or local/.... It does not need to resolve on the network unless you plan to publish or fetch it remotely.
  • Match your repository path when you intend to push the code to a VCS host. For example, if your repo will be github.com/alex/moddemo, use go mod init github.com/alex/moddemo.
  • Major version in path for v2+: modules at major version 2 or higher typically include the major version suffix in the module path (for example, github.com/user/project/v2).

Inside your module, packages are imported using the module path plus the package subdirectory. For example, a package in ./internal/mathutil would be imported as example.com/moddemo/internal/mathutil (though internal has special visibility rules).

2) Add a dependency with an import + go mod tidy (roles of go.mod and go.sum)

In module mode, you typically add dependencies by importing them in code and then letting Go resolve versions. The command go mod tidy is the standard way to synchronize your module files with what your code actually imports.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Step-by-step: add a third-party dependency

Create a small program that uses a third-party package. This example uses a common CLI helper package (github.com/spf13/cobra) to show a realistic dependency:

mkdir -p cmd/moddemo
cat > cmd/moddemo/main.go <<'EOF'
package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	root := &cobra.Command{
		Use:   "moddemo",
		Short: "A tiny CLI to demonstrate Go modules",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("hello from modules")
		},
	}

	_ = root.Execute()
}
EOF

Now resolve dependencies and update module files:

go mod tidy

After this, you should see:

  • go.mod updated with a require entry for the module(s) you imported.
  • go.sum created/updated with cryptographic hashes for the module content you downloaded.

What go.mod does

go.mod is the manifest for your module. It records:

  • Your module path (the module ... line).
  • The Go language version used for module semantics (the go ... line).
  • Required dependencies and their selected versions (require directives).
  • Optional directives such as replace (override where a dependency is sourced) and exclude (avoid a bad version).

What go.sum does

go.sum is an integrity log. It stores checksums for specific module versions (and often their go.mod files) that your build has used. This helps ensure that when you (or CI) fetch dependencies again, you get the same bits, not a modified archive with the same version tag.

Important practical point: it is normal for go.sum to contain entries for transitive dependencies (dependencies of your dependencies). It can also contain more entries than go.mod because it records what was downloaded and verified over time.

3) Update and pin versions with go get (and semantic versioning expectations)

Once you have dependencies, you will sometimes need to upgrade, downgrade, or pin them to a specific version. In modern Go, version selection is still module-based, but the recommended commands vary slightly by Go version and intent.

go get behavior (what to know)

  • In current Go releases, go get is primarily used to change module requirements (add/upgrade/downgrade dependencies) in your go.mod.
  • Installing executables is typically done with go install pkg@version (not go get), but for library dependencies in your module, go get remains a common tool.

Pin a dependency to an exact version

To set a specific version (for example, pin cobra to a chosen version):

go get github.com/spf13/cobra@v1.7.0

This updates go.mod to require that version (or a compatible selection based on the module graph), then you can run:

go mod tidy

Running tidy after version changes is a good habit to keep go.mod and go.sum consistent with your actual imports.

Upgrade to the latest compatible version

To upgrade a dependency to the latest available version:

go get github.com/spf13/cobra@latest

To upgrade all direct and indirect dependencies (use carefully in real projects):

go get -u ./...

After upgrading, run tests/builds to confirm compatibility, then tidy:

go test ./...
go mod tidy

Downgrade a dependency

If a newer version causes issues, you can downgrade explicitly:

go get github.com/spf13/cobra@v1.6.1
go mod tidy

Semantic versioning expectations (what Go assumes)

Go modules follow semantic versioning (SemVer) conventions:

  • vMAJOR.MINOR.PATCH (for example, v1.7.0).
  • PATCH releases should be backward compatible bug fixes.
  • MINOR releases may add functionality but should remain backward compatible within the same major version.
  • MAJOR releases may introduce breaking changes; in Go modules, v2+ typically changes the module path (for example, .../v2), making breaking upgrades explicit in imports.

Go also uses Minimal Version Selection (MVS): it selects the minimum version of each module that satisfies all requirements in the dependency graph. This means your build is stable and predictable, but it also means upgrading one dependency can indirectly raise versions of others if required.

4) Reproducible builds: why go.sum matters and how the module cache works

Reproducible builds mean that the same source code and module files produce the same dependency inputs across machines and over time. In Go module workflows, two key pieces support this: checksum verification (go.sum) and the module download cache.

Why go.sum is critical

  • Integrity verification: when Go downloads a module version, it verifies the content against the checksum recorded in go.sum. If the downloaded content does not match, the build fails rather than silently using altered code.
  • Team consistency: committing go.sum ensures that teammates and CI verify the same module content you verified.
  • Defense against mutable tags: if a tag is moved or a proxy serves different content for the same version, checksum mismatches surface immediately.

Practical rule: commit both go.mod and go.sum to version control.

Module cache at a high level

When you build or tidy, Go downloads module versions into a shared module cache (typically under $GOMODCACHE, often inside $GOPATH/pkg/mod). Key ideas:

  • Cached by module@version: the cache stores immutable snapshots of module versions, so repeated builds are faster and offline-friendly once dependencies are present.
  • Read-only module directories: cached module directories are treated as read-only to discourage accidental edits to dependency code.
  • Build uses cache + sums: the go command fetches modules (directly or via a proxy), verifies checksums, then uses the cached content for compilation.

If you need to see where Go is getting modules from and what it is doing, you can use:

go env GOMODCACHE
go env GOPATH

5) Practical workflow: keep the graph clean, remove unused deps, troubleshoot common errors

Clean up dependencies with go mod tidy

go mod tidy is the main maintenance command:

  • Adds missing module requirements needed to build and test your packages.
  • Removes requirements that are no longer necessary.
  • Updates go.sum entries to match what is actually needed and verified.

Typical workflow after code changes:

go test ./...
go mod tidy
go test ./...

Remove unused dependencies

In module mode, you usually remove dependencies by removing the imports (or deleting code) that uses them, then running:

go mod tidy

This will drop unused require lines from go.mod and clean related go.sum entries over time.

Inspect why a module is included

If you see a dependency you don’t expect, you can ask Go why it is in the graph:

go mod why -m github.com/spf13/cobra

This prints a shortest path of imports that pulls that module in.

Troubleshoot common module errors

Error: “no required module provides package …”

  • Cause: you imported a package path that is not in your current module requirements.
  • Fix: run go mod tidy or go get module@version for the module that provides that package.
  • Also check: typos in the import path, or importing a package that moved between major versions (v2+ path suffix).

Error: “module declares its path as X but was required as Y”

  • Cause: the module’s own module path in its go.mod does not match the path you’re requiring/importing (often due to v2+ path suffix issues or forks).
  • Fix: import/require the correct module path (including /v2 when applicable). If you must use a fork or local copy, use a replace directive carefully.

Error: “checksum mismatch”

  • Cause: downloaded module content does not match the checksum in go.sum.
  • Fix: treat this seriously. It can be caused by a proxy issue, a moved tag, or tampering. Try cleaning the module download for that module version and re-fetching, or use a different proxy configuration. Do not “fix” it by blindly deleting go.sum in a real project; understand why it changed.

Error: private repo access failures

  • Cause: Go cannot authenticate to fetch a private module.
  • Fix: configure your VCS credentials and environment variables for private modules (for example, settings related to private module fetching). In team settings, document the expected configuration.

Exercise: add a small dependency and verify module files

Goal: add a CLI helper dependency, ensure go.mod and go.sum reflect it, and confirm the project builds.

  • 1) In your module, create a new command under cmd/exercise that imports a CLI helper such as github.com/spf13/pflag or github.com/spf13/cobra.
  • 2) Run go mod tidy.
  • 3) Open go.mod and confirm there is a require entry for the module you imported.
  • 4) Open go.sum and confirm it contains checksum lines for that module version.
  • 5) Build and run the command:
go build ./...
./exercise --help

If the build succeeds and both module files are updated without extra unused requirements, your module setup and dependency workflow are correct.

Now answer the exercise about the content:

After you add a new third-party import to your Go code, what is the main purpose of running go mod tidy?

You are right! Congratulations, now go to the next page

You missed! Try again.

go mod tidy updates go.mod and go.sum to match real imports: it adds needed module requirements, removes unused ones, and refreshes checksums for downloaded module versions.

Next chapter

Slices and Arrays in Go: Working with Collections Safely and Efficiently

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.