4 min read
On this page

Modules & Dependencies

Go modules are the dependency management system for Go, introduced in Go 1.11 and the default since Go 1.16. A module is defined by a go.mod file at its root, which declares the module path, the Go version, and all dependencies. Together with go.sum, modules provide reproducible, verifiable builds.

go.mod: The Module Definition

Every Go project starts with go mod init.

// go.mod
module github.com/myorg/myservice

go 1.22

require (
    github.com/jackc/pgx/v5 v5.5.3
    github.com/go-chi/chi/v5 v5.0.12
    go.uber.org/zap v1.27.0
)

require (
    // indirect dependencies (dependencies of your dependencies)
    github.com/jackc/pgpassfile v1.0.0 // indirect
    github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
    go.uber.org/multierr v1.11.0 // indirect
)

The module directive declares the import path for your module. The go directive specifies the minimum Go version. The require blocks list dependencies with their exact versions.

Direct dependencies are ones your code imports. Indirect dependencies are pulled in transitively. Go separates them for clarity.

go.sum: Checksums for Reproducibility

The go.sum file contains cryptographic checksums for every module version used in your build.

github.com/jackc/pgx/v5 v5.5.3 h1:...
github.com/jackc/pgx/v5 v5.5.3/go.mod h1:...
github.com/go-chi/chi/v5 v5.0.12 h1:...
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:...

This file ensures that everyone building your project gets exactly the same code. If a checksum does not match, the build fails. Always commit go.sum to version control.

Adding Dependencies with go get

Use go get to add or update dependencies.

# Add a new dependency
go get github.com/google/uuid

# Add a specific version
go get github.com/google/uuid@v1.6.0

# Update to the latest minor/patch version
go get -u github.com/google/uuid

# Update all dependencies
go get -u ./...

After running go get, the dependency appears in go.mod and its checksums are recorded in go.sum.

You can also just write the import in your code and run go mod tidy — it will fetch the latest version automatically.

go mod tidy: Cleanup

go mod tidy is the command you will run most often. It does two things: adds any missing dependencies that your code imports, and removes any dependencies that your code no longer uses.

go mod tidy

Run this after adding or removing imports, after merging branches, or whenever go.mod feels out of sync with your code. It is safe to run at any time.

# Typical workflow
# 1. Write code that imports a new package
# 2. Run tidy to fetch it
go mod tidy

# 3. Verify everything builds
go build ./...

Major Version Suffixes

Go modules follow semantic versioning with a twist: major versions v2 and above require a suffix in the module path.

# v0 and v1: no suffix
require github.com/myorg/mylib v1.4.2

# v2 and above: suffix in the path
require github.com/myorg/mylib/v2 v2.1.0

# v3
require github.com/myorg/mylib/v3 v3.0.0

This means the import path changes when the major version changes.

// Importing v1
import "github.com/myorg/mylib"

// Importing v2
import "github.com/myorg/mylib/v2"

You can import both v1 and v2 in the same project. They are treated as completely separate packages. This is how Go handles breaking changes — the import path makes the version explicit.

Vendoring with go mod vendor

Vendoring copies all dependencies into a vendor/ directory inside your project.

go mod vendor

This creates a vendor/ directory with the source code of every dependency. When you build with -mod=vendor or have GOFLAGS=-mod=vendor set, Go uses the vendored copies instead of the module cache.

myproject/
  go.mod
  go.sum
  vendor/
    github.com/
      google/
        uuid/
          uuid.go
          ...
    modules.txt
  main.go

When to Vendor

CI reproducibility. Vendoring ensures your CI does not depend on external module proxies being available. The build works even if proxy.golang.org is down.

Air-gapped environments. In environments without internet access, vendoring is the only way to build.

Auditing. Having all dependency source code in your repository makes it easier to audit and review third-party code.

When not to vendor. For libraries published as Go modules, vendoring is usually unnecessary. The module system already provides reproducibility through checksums. Vendoring adds significant noise to your repository.

The Module Proxy & GOPROXY

Go fetches modules through a proxy, defaulting to proxy.golang.org. The proxy caches modules, ensuring availability even if the original repository is deleted.

# Default: use Google's proxy, fall back to direct
GOPROXY=https://proxy.golang.org,direct

# Use a private proxy for internal modules
GOPROXY=https://internal-proxy.company.com,https://proxy.golang.org,direct

# Skip the proxy for private modules
GONOSUMCHECK=github.com/myorg/*
GONOSUMDB=github.com/myorg/*
GOPRIVATE=github.com/myorg/*

For private repositories, set GOPRIVATE so Go does not try to fetch them through the public proxy or check them against the public checksum database.

Replacing & Excluding Dependencies

The replace directive overrides where Go finds a module. This is useful for local development or forking a dependency.

// go.mod

// Use a local copy during development
replace github.com/myorg/sharedlib => ../sharedlib

// Use a fork
replace github.com/broken/lib => github.com/myfork/lib v1.2.3

The exclude directive prevents a specific version from being used.

// go.mod
exclude github.com/buggy/lib v1.3.0

Both directives only apply to the main module — they are ignored when your module is used as a dependency by someone else.

Workspaces for Multi-Module Development

Go 1.18 introduced workspaces for developing multiple modules simultaneously. A go.work file at the root connects them.

// go.work
go 1.22

use (
    ./api
    ./shared
    ./worker
)

This lets you make changes to shared/ and immediately see them in api/ and worker/ without publishing or using replace directives.

monorepo/
  go.work
  api/
    go.mod
    main.go
  shared/
    go.mod
    types.go
  worker/
    go.mod
    main.go

Do not commit go.work to version control unless your entire team uses the same workspace layout. It is primarily a local development tool.

Common Pitfalls

  • Not running go mod tidy. Stale dependencies in go.mod cause confusion. Run tidy regularly, especially before committing.
  • Forgetting to commit go.sum. Without go.sum in version control, other developers and CI cannot verify dependency integrity.
  • Using replace in library modules. The replace directive is ignored when your module is a dependency. It is only for the main module. Use it for local development, not as a permanent solution.
  • Ignoring major version suffixes. If a dependency releases v2, you must update the import path to include /v2. Simply changing the version number in go.mod is not enough.
  • Vendoring and forgetting to update. After changing dependencies, run go mod vendor again. Stale vendor directories cause mysterious build failures.

Key Takeaways

  • go.mod defines your module path, Go version, and dependencies.
  • go.sum provides checksums for reproducible builds — always commit it.
  • Use go get to add dependencies and go mod tidy to clean up.
  • Major versions v2+ require a path suffix (/v2, /v3).
  • Vendor with go mod vendor for CI reproducibility or air-gapped environments.
  • Set GOPRIVATE for internal modules that should not go through the public proxy.
  • Use workspaces for local multi-module development.