Package and Module Commands

Detailed guide to managing Go dependencies, inspecting packages, maintaining modules, and using workspaces with go get, go list, go mod, and go work

Go ships with four essential commands that handle dependencies, package inspection, module housekeeping, and multi-module development. Each one solves a distinct problem in the workflow: go get brings in external libraries, go list answers questions about what is in your code, go mod keeps the module definition healthy, and go work lets you work across several modules at once without touching go.mod files. This section covers all four in depth, with practical examples and the common mistakes that trip up new Gophers.

go get – Adding and Managing Dependencies

go get resolves a package path, fetches the corresponding module, records the exact version in go.mod and go.sum, and makes the package available for import in your code. It is the primary command for adding, upgrading, and downgrading module dependencies.

What go get Actually Does

When you run go get golang.org/x/exp/slices, the go command:

  1. Resolves the package path to a module (here, golang.org/x/exp).
  2. Determines the latest version tag that is compatible with the current module’s go directive.
  3. Downloads that version of the module to the module cache.
  4. Writes a require directive into go.mod with the chosen version.
  5. Adds checksum entries to go.sum to guarantee future downloads return identical bytes.
  6. Makes the package’s exported identifiers available to any file in the current module.

You can specify versions explicitly with an @version suffix. The most useful forms are:

  • @latest – the latest tagged stable release
  • @v1.2.3 – a specific semantic version
  • @commit-hash – a specific commit (often used for unreleased fixes)
  • @none – removes the dependency
# Add the latest version of the exp module
go get golang.org/x/exp@latest
# Pin a specific version
go get github.com/google/uuid@v1.3.0
# Use a particular commit
go get github.com/lib/pq@abc1234

After a go get, the require block in go.mod records the dependency with its version, and go.sum stores the cryptographic hashes of the module’s content.

go get vs. go install – Do Not Mix Them Up:

Since Go 1.16, go get is only for managing dependencies in the current module. To install a command-line tool globally, use go install <package>@latest. Running go get to install a binary outside a module can silently modify the module’s go.mod and go.sum, which is almost never what you want.

Upgrading and Downgrading Dependencies

go get can also shift an existing dependency to a new version. It updates the require directive automatically.

# Upgrade to the latest patch release (v1.3.x → latest v1.3.y)
go get github.com/google/uuid@v1.3
# Upgrade to the latest minor/patch (v1.x.y → latest v1.x.y)
go get -u github.com/google/uuid
# Upgrade to the latest major (v2+) — requires module path change
go get github.com/google/uuid/v2@latest

For major versions v2 and above, the module path must include the suffix /v2 (or /v3, etc.). go get handles the import path rewriting implicitly; all your imports must also use the new path.

Downgrading works the same way: specify an older version.

go get github.com/google/uuid@v1.2.0

A successful go get modifies both go.mod and go.sum. It does not remove dependencies that become unused — that is the job of go mod tidy.

Removing a Dependency

To remove a dependency entirely, use the special version @none:

go get github.com/legacy/tool@none

This deletes the require line from go.mod and cleans up the corresponding entries in go.sum. However, if another imported package still transitively requires that module, the require line may stay in the // indirect section.

Indirect Dependencies Are Normal:

Modules that are not directly imported by your code but are needed by your direct dependencies appear in go.mod with an // indirect comment. The go tooling manages them for you; you rarely need to touch them manually.

Common Flags

  • -d – only download, do not build or install packages. Useful for pre-populating the module cache.
  • -u – update the named packages and their dependencies. When used with @version, -u allows updating to the specified version even if it requires pulling in newer transitive dependencies.
  • -t – also update or add modules required by tests of the named packages. Without this flag, test-only dependencies might not be added.

How Beginners Should Think About go get

Picture a library shelf. go get is the librarian that fetches the exact edition of a book you request and writes the title and edition number (go.mod) and a tamper-proof label (go.sum) into your project log. From then on, your code can reference that book by its title (import path) and trust that the same content will always be on the shelf.

A common rookie mistake is to go get a package and then commit go.mod but forget go.sum. The go.sum file is a security and reproducibility contract — always commit it with your source code.

go list – Inspecting Packages and Modules

go list answers questions like “what packages are in my module?”, “what are all my dependencies?”, and “what versions of a particular module are available?”. It is the swiss‑army knife for introspection and is widely used in scripts and editor integrations.

Basic Package Listing

Without any flags, go list shows the import path of the packages named by its arguments. The special pattern ./... expands to all packages in and under the current directory.

go list ./...                    # all packages in the current module
go list ./handlers/...          # packages under handlers/

The output is just import paths, one per line:

github.com/you/proj
github.com/you/proj/handlers
github.com/you/proj/models

Listing Module Information

The -m flag switches go list into module mode. Now it operates on modules rather than packages.

go list -m all          # current module and all dependencies
go list -m -versions github.com/gin-gonic/gin   # available versions

The output of go list -m all is a flat list, with the main module first and dependencies sorted by module path:

github.com/you/proj
github.com/bytedance/sonic v1.9.1
github.com/gin-gonic/gin v1.9.1
...

To see the module that provides a specific package, use go list -m with the package path:

go list -m golang.org/x/tools/cmd/goimports
# outputs: golang.org/x/tools v0.17.0

Custom Formatting with -f

The -f flag accepts a Go template that lets you extract any field from the Package or Module struct. This is extremely powerful for scripting.

# Print the directory where each package lives
go list -f '{{.Dir}}' ./...
# Print the module version of each dependency
go list -m -f '{{.Path}} {{.Version}}' all
# Show the full GOPATH target for the main package (used for go install)
go list -f '{{.Target}}'

A common pattern is to use go list to find the directory of a tool so you can add it to $PATH after a go install:

export PATH=$PATH:$(dirname $(go list -f '{{.Target}}' .))

Real-World Use: Checking for Stale Dependencies

By combining go list with module mode, you can quickly spot outdated indirect dependencies or modules that are pulling in unexpected versions.

go list -m -u all

The -u flag adds information about available upgrades. The output will mark modules with [v1.2.3] if a newer version exists.

No Output Means Everything Is in Order:

After running go mod tidy, if go list -m -u all shows no lines with [v...], every dependency is at the latest version allowed by your constraints. Your module is clean.

When to Use go list

  • Understanding why a large module was pulled in — use go mod graph and go mod why for deeper analysis, but go list -m gives a quick overview.
  • Automating build scripts where you need a package’s directory or its resolved module version.
  • Preparing CI pipelines that verify only approved module versions are present.

Forgetting the -m Flag:

go list without -m operates on packages. If you are trying to list module versions and forget -m, you will see package import paths instead, which can lead to confusion in scripts. Always double-check which domain you are querying.

go mod – Module Maintenance

go mod is the top-level command for everything related to the go.mod file: initializing a new module, pruning unused dependencies, vendoring, verifying checksums, and examining the dependency graph. Unlike go get, which manipulates dependencies implicitly, go mod gives you explicit, fine-grained control.

go mod init – Starting a New Module

Every Go module begins with a go.mod file in its root directory. go mod init <module-path> creates that file and populates it with the module path and the current go version.

mkdir myproject && cd myproject
go mod init github.com/you/myproject

This generates a minimal go.mod:

module github.com/you/myproject
go 1.21

The module path should be something globally unique, typically the URL of the repository that will host the code (e.g., github.com/you/myproject). You do not need to publish the code for the module to work locally; the path merely acts as a namespace for your packages.

Module Initialized:

If you see go: creating new go.mod: module github.com/you/myproject, the initialization succeeded. You can now write Go source files and import other packages.

go mod tidy – Cleaning Up Dependencies

go mod tidy scans all source files (including test files) and ensures go.mod and go.sum accurately reflect every import. It performs three critical actions:

  1. Adds any module that provides an imported package and is not yet present in go.mod.
  2. Removes any require directive for a module that is no longer imported.
  3. Updates go.sum to match.

You should run go mod tidy after adding or removing imports, after a go get, and before committing changes.

go mod tidy

Typical output when new dependencies are fetched:

go: finding module for package github.com/rs/zerolog
go: found github.com/rs/zerolog in github.com/rs/zerolog v1.32.0

After it completes, go.mod and go.sum are accurate. This is the single most important hygiene command.

Skipping tidy Leaves Orphaned Requirements:

If you remove an import from your code but never run go mod tidy, the module requirement remains in go.mod. This inflates the dependency list and can cause go build to download unused modules. It also misleads anyone inspecting your module about its real dependencies.

go mod vendor – Creating a Local Copy of Dependencies

go mod vendor copies all packages that the module needs into a vendor directory at the module root. The go command can then build using those local copies instead of downloading from the network. Vendoring is useful for:

  • Environments with no internet access.
  • Guaranteeing that a build uses exactly the versions that are checked into version control.
  • Projects that need to ship with all source code for license compliance.
go mod vendor

After running this, building with go build -mod=vendor forces the use of the vendor directory. Many older projects and internal corporate setups rely on vendoring.

Vendor Directory Drift:

The vendor directory is a copy — it does not update automatically. Every time go.mod changes (after go get or go mod tidy), you must re‑run go mod vendor to keep them in sync. An out‑of‑date vendor directory causes mysterious build failures.

go mod verify – Trust but Check

go mod verify checks that every module in the module cache matches the hashes in go.sum. It downloads nothing; it only verifies cached content.

go mod verify

If all modules pass, the output is simply:

all modules verified

If a module’s content has been tampered with or corrupted, the command fails with an error describing the mismatch. This is an essential step in CI pipelines to detect supply‑chain attacks.

go mod download – Pre-fetching Modules

go mod download downloads all modules required by go.mod into the module cache without modifying any files. It is used to warm up the cache in Docker layers, CI systems, or offline builds.

go mod download

The command is silent on success. If you want to see what it does, add -x.

go mod edit – Programmatic go.mod Manipulation

go mod edit provides flags to change go.mod from the command line without manually editing the file. It is rarely used by humans directly, but tools and scripts rely on it.

# Replace a module with a local version
go mod edit -replace=github.com/old/dep=../local/dep
# Drop a replace directive
go mod edit -dropreplace=github.com/old/dep
# Set the Go version
go mod edit -go=1.22

These edits are precise and avoid accidentally breaking the file’s formatting. Most developers will never need this command, but knowing it exists helps when reading Makefiles and generator scripts.

go mod graph and go mod why – Understanding the Dependency Web

The module requirement graph can become surprisingly complex. Two commands help you navigate it.

go mod graph prints the entire dependency graph as <module> <required-module>@version pairs.

go mod graph

Output (truncated):

github.com/you/proj github.com/gin-gonic/gin@v1.9.1
github.com/gin-gonic/gin@v1.9.1 github.com/gin-contrib/sse@v0.1.0
...

This raw output is useful for piping into tools like grep or for visualizing the graph with third‑party packages.

go mod why answers the question: why is module X needed? It finds a shortest import path from your code to any package in that module.

go mod why github.com/ugorji/go/codec

It might output:

# github.com/ugorji/go/codec
github.com/you/proj
github.com/gin-gonic/gin
github.com/gin-gonic/gin/binding
github.com/ugorji/go/codec

If no path exists, the module is listed as not needed, and go mod tidy can safely remove it.

How Beginners Should Think About go mod

Think of go.mod as the recipe for your project’s dependency cake. go mod init writes the first draft, go get adds ingredients, and go mod tidy makes sure the recipe doesn’t list flour you never use. The other subcommands (vendor, verify, graph) are quality‑control inspectors and stock‑takers that keep the cake safe and reproducible.

go work – Multi-Module Development with Workspaces

Go 1.18 introduced workspaces to solve a common pain point: when you are simultaneously working on a library module and an application that consumes it, the traditional approach required replace directives in go.mod — directives that you must remember to remove before committing. Workspaces eliminate that friction by defining a separate go.work file that lists the modules you are actively developing together.

The Problem Workspaces Solve

Without workspaces, a replace directive in the application’s go.mod would point to the local library:

replace github.com/you/mylib => ../mylib

This works, but it is easy to accidentally commit the replace line. Workspaces move the local reference out of go.mod entirely.

Creating a Workspace

A workspace consists of a go.work file in some parent directory. That file declares which modules are part of the workspace and optionally pins a Go toolchain version.

# Navigate to a directory that will contain both modules
mkdir workspace && cd workspace
# Initialize the workspace
go work init ./app ./lib

This creates a go.work file:

go 1.21
use (
    ./app
    ./lib
)

Now, any go command run inside the app directory resolves imports against the local lib directory — no replace needed.

Adding a Module Later

go work use adds a directory to an existing workspace:

go work use ./another-lib

If a module listed in go.work is removed from disk, you must manually drop it with go work edit -dropuse <path>, or just re‑initialize the workspace.

Synchronizing Dependencies

go work sync ensures that every module in the workspace shares a consistent set of dependency versions. It updates each module’s go.mod so that common dependencies are resolved to the same version across the workspace.

go work sync

This prevents “works‑in‑my‑module, breaks‑in‑yours” version mismatches during joint development.

What Workspaces Are Not

Workspaces are a local development convenience. They are not a deployment mechanism. You should never commit go.work to version control; the file contains paths that are specific to your machine. When you are ready to release, remove the workspace and let the modules resolve their dependencies normally.

Workspace Files Are Local Only:

Add go.work to your .gitignore. The workspace tracks a development view; consumers of your modules will never see it and should not need it.

Practical Walkthrough

Imagine you maintain a reusable library github.com/you/config and a microservice github.com/you/api that imports it. During development, you fix a bug in the config library and want to see the effect in the API immediately — without pushing to a remote and pulling.

workspace/
├── go.work
├── config/
│   └── go.mod   (module github.com/you/config)
└── api/
    └── go.mod   (module github.com/you/api, imports github.com/you/config)

With the workspace in place, a go build inside api/ automatically uses the local config directory. No replace directive appears in either go.mod. When you commit and push, the CI pipeline resolves github.com/you/config from the remote, and everything works identically.

This workflow dramatically reduces the accidental‑commit problem and makes multi‑module development feel natural.

Summary

The four commands form a coherent pipeline for dependency management and code exploration. go get brings external code into your project; go mod keeps the project’s dependency definition clean, verifiable, and reproducible; go list lets you inspect what is actually there; and go work smooths the rough edges of working across module boundaries. They share the same go.mod-centric view of the world, so understanding one command deepens your grasp of the others.

The one habit that ties them all together is running go mod tidy after any change to imports. That single command ensures that go get additions, manual removals, and workspace experiments all leave go.mod in a state that builds reproducibly anywhere.