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.modcause confusion. Runtidyregularly, especially before committing. - Forgetting to commit go.sum. Without
go.sumin version control, other developers and CI cannot verify dependency integrity. - Using replace in library modules. The
replacedirective 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 ingo.modis not enough. - Vendoring and forgetting to update. After changing dependencies, run
go mod vendoragain. Stale vendor directories cause mysterious build failures.
Key Takeaways
go.moddefines your module path, Go version, and dependencies.go.sumprovides checksums for reproducible builds — always commit it.- Use
go getto add dependencies andgo mod tidyto clean up. - Major versions v2+ require a path suffix (
/v2,/v3). - Vendor with
go mod vendorfor CI reproducibility or air-gapped environments. - Set
GOPRIVATEfor internal modules that should not go through the public proxy. - Use workspaces for local multi-module development.