Dependency Management

Updated

September 13, 2026

Dependency Management

A module lists what it needs in go.mod and records checksums in go.sum. The boring default is the module cache plus a proxy, not a vendor/ tree. This chapter’s runnable programs use only the standard library so they work offline. go get is shown as a command you will type when you do add a module.

Mental model

go.mod has a module path, a language version (go 1.27), and require lines for other modules. go.sum is a lock of cryptographic hashes. Commit both.

The module cache lives under GOMODCACHE (usually $GOPATH/pkg/mod). Builds read from there. GOPROXY (default https://proxy.golang.org,direct) is how the toolchain fetches code you do not have yet.

go get adds or upgrades a require. go mod tidy adds what the source imports and drops what it does not. Versions are semantic (v1.2.3). retract in an upstream go.mod marks a version you should not use. vendor/ is an optional snapshot for networks that cannot reach a proxy — not the everyday layout.

Worked examples

Case 1: A module that only uses the standard library

Empty directory. Save go.mod:

module example.com/desk

go 1.27

Save as main.go:

// main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Fprintf(os.Stdout, "desk module %s\n", "example.com/desk")
}

Run:

go run .
go list -m

Output:

desk module example.com/desk
example.com/desk

There is no require block. There may be no go.sum, or an empty one. That is correct: the standard library is not a module you download. go.sum grows when you add a module dependency.

Case 2: Stdlib packages still belong in the import block, not in go.mod

Save as menu.go next to the same go.mod. JSON is in the standard library. It does not get a require line.

// menu.go
package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Item struct {
    Name  string `json:"name"`
    Price int    `json:"price"`
}

func main() {
    item := Item{Name: "tea", Price: 3}
    err := json.NewEncoder(os.Stdout).Encode(item)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

This file is a second package main in the same directory as Case 1. Keep one main.go when you run .. For this case, run the file:

go run menu.go

Output:

{"name":"tea","price":3}

encoding/json is part of the toolchain. If you find yourself go get-ing a JSON library on day one, stop. Read the stdlib first.

Case 3: go get, go mod tidy, versions (commands)

When you do need an external module, the commands look like this. Do not run them for this chapter’s programs; they need the network and a real module.

go get example.com/mod@v1.2.3
go get example.com/mod@latest
go get example.com/mod@none
go mod tidy

@v1.2.3 pins a version. @latest takes the highest release the proxy knows (not a pseudo-version from the default branch unless there are no tags). @none drops the dependency from go.mod.

go mod tidy is the closer: it reads your imports, adds missing require lines, removes unused ones, and updates go.sum. Run it after you add or delete an import.

A go.mod with a dependency looks like this (illustrative — this module is not fetched here):

module example.com/desk

go 1.27

require example.com/mod v1.2.3

go.sum then has hash lines for example.com/mod and its go.mod. Never edit go.sum by hand. If it is dirty, run go mod tidy or go get again.

Case 4: retract, and why not to vendor by default

Upstream authors mark bad versions in their go.mod:

retract v1.0.2 // published with a broken zip

or:

retract [v1.0.0, v1.0.2] // all of these builds are wrong

go get example.com/mod@v1.0.2 then warns. You pick a version that is not retracted. You do not copy retract lines into your own module unless you are the author who shipped the bad tag.

go mod vendor copies required modules into vendor/. Builds can use -mod=vendor. That is useful when CI has no network and no module cache. It is not useful as the default: the directory bloats the repository, every upgrade is a huge diff, and go.sum already pins content. Prefer a proxy and a warm cache. Vendor when a real constraint forces you to.

Case 5: The replace directive for local development

When developing a library alongside an application, or patching a bug in a dependency before upstream merges your fix, add a replace directive to go.mod:

module example.com/desk

go 1.27

require example.com/store v1.2.0

replace example.com/store => ../store

The toolchain ignores the version in the module cache and compiles directly against the files in ../store. You can edit both without publishing intermediate git tags.

When done testing, drop the replace directive before pushing your branch:

go mod edit -dropreplace example.com/store

Case 6: Major version upgrades (/v2, /v3)

In Go’s module system, Semantic Versioning is part of the import path. A major version bump (from v1 to v2) represents a breaking API change. Go enforces the Import Compatibility Rule: if an old API and a new API have the same import path, the new API must be backwards-compatible with the old one.

Therefore, breaking releases require a major version suffix in the module declaration:

module example.com/desk/v2

go 1.27

Callers import the new major version explicitly:

import "example.com/desk/v2/orders"

Because example.com/desk and example.com/desk/v2 are distinct module paths, a project can import both simultaneously during migration without version conflicts or dependency collisions.

The trap

Hand-editing go.mod versions until the build “seems” to work, deleting go.sum because it looks noisy, or vendoring on day one “for reproducibility.” Reproducibility is go.sum plus the proxy. This program needs no extra module. The trap is adding one anyway.

// leftover.go
package main

import "fmt"

func main() {
    fmt.Println("still stdlib")
}

Run:

go run leftover.go

Output:

still stdlib

If go.mod still requires a module this file does not import, go mod tidy will drop it. That is the point. A leftover require is a leftover attack surface and a leftover upgrade. Do not keep dependencies “in case we need them.”

The other trap: replace lines pointed at a coworker’s laptop path, committed forever. replace is a local override. Workspaces (next chapter) are the cleaner way to develop two modules at once.

The boring rule

  • go.mod and go.sum are source. Commit them.
  • Stdlib is not a require. Import it; do not go get it.
  • go get pkg@version to add or upgrade. go mod tidy to reconcile.
  • Major breaking changes require updating the module path suffix (/v2).
  • Use replace only for local temporary debugging or offline forks; never commit a replace targeting local filesystem paths.
  • Pin versions you care about. Read retract warnings; do not ignore them.
  • Do not vendor by default. Do not delete go.sum. Do not leave unused requires.

Try this

  1. In Case 1, run go env GOMODCACHE GOPROXY. Read the two paths.
  2. Run go list -m all in that module. You should see only example.com/desk.
  3. Add an unused import of strings to main.go, run go run . (it should fail), then remove the import and run go mod tidy. Confirm go.mod still has no require block.
  4. Run go list -m -u all in any project with dependencies to see available minor and patch updates.