Go Modules and Package Management
A practical guide to Go modules: go.mod, go.sum, semantic versioning, replace directives, and the commands you actually need day to day.
What you'll learn
- ✓What a Go module is and how it relates to packages
- ✓The roles of go.mod and go.sum
- ✓Adding, upgrading, and pinning dependencies
- ✓Minimum Version Selection (MVS) explained
- ✓Replace and vendoring workflows for local development
Prerequisites
- •Basic familiarity with Go syntax and go run
What and Why
A Go module is a collection of related packages versioned together. It is the unit of distribution and dependency in modern Go. Every project has exactly one go.mod at its root that records the module path, the Go version, and direct dependencies.
Modules replaced the older GOPATH workflow. They give you reproducible builds, semantic versioning, and a build-time guarantee that everyone gets the same code via go.sum.
Mental Model
my-project/
go.mod # module path, deps, go version
go.sum # cryptographic hashes for every used module
main.go # package main
internal/... # private to this module
pkg/... # public packages
go.mod
module example.com/my-project
go 1.22
require (
github.com/foo/bar v1.4.2
golang.org/x/sync v0.7.0
) When you run go build, Go reads go.mod, resolves the full dependency graph using Minimum Version Selection (MVS), downloads modules to the module cache, and verifies them against go.sum.
Hands-on Example
A typical workflow looks like this.
# Create a new module
mkdir greeter && cd greeter
go mod init example.com/greeter
# Write some code that uses a dependency
cat > main.go <<'EOF'
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
fmt.Println("hello", uuid.New())
}
EOF
# Resolve and record the dependency
go mod tidy
# Build and run
go run .
go mod tidy is the workhorse: it scans your source, adds missing requirements, removes unused ones, and refreshes go.sum.
Upgrading a dependency:
go get github.com/google/uuid@latest # latest tagged release
go get github.com/google/uuid@v1.6.0 # specific version
go get -u ./... # update everything used by current package
To pin a transitive dependency without using it directly, add an explicit require line and run go mod tidy.
Common Pitfalls
- Module path mismatch: the
moduleline must match the import path consumers use. Renaming a repository without updatinggo.modbreaks downstream builds. - Major versions in the path: starting at v2, the major version becomes part of the import path:
github.com/foo/bar/v2. Forgetting this leads to compilation errors users cannot decipher. - Editing go.sum by hand: never. It is regenerated by
go mod tidy. Manual edits get overwritten. go getoutside a module: in module-aware mode,go getmodifiesgo.mod. To install a binary tool globally, usego install example.com/tool@latest.- Replace without commit: a
replacedirective pointing to a local path is great for development but should not be committed tomain. CI builds elsewhere will fail.
Practical Tips
Use replace to develop against a local copy of a dependency:
replace github.com/me/lib => ../lib
Run go mod tidy and your tests against the local code. Drop the replace before publishing.
Set GOPROXY thoughtfully. The default https://proxy.golang.org,direct is great for public modules. For private modules, configure GOPRIVATE so the proxy isn’t queried:
go env -w GOPRIVATE=github.com/my-org/*
Use internal/ for packages you do not want consumers to import. Anything under internal/ is only importable by code within the same module subtree. This is the strongest visibility boundary Go offers.
For reproducible builds, commit both go.mod and go.sum. For airgapped builds, go mod vendor copies dependencies into a vendor/ directory you can ship.
When upgrading a major version, read the changelog. Major bumps are explicit because they change the import path, but transitive upgrades pulled in by go get -u can still surprise you.
Wrap-up
Go modules give you a small surface area: one go.mod per project, one command (go mod tidy) to keep it in shape, and Minimum Version Selection to keep upgrades predictable. Lean on the standard workflow, use replace only for local development, and respect the internal/ boundary for private APIs. With these habits, dependency management stops being a source of surprise and becomes a quiet part of your build pipeline.
Related articles
- Go Go Build Tags Explained
Use Go build tags to include or exclude files per OS, architecture, or custom condition. Learn the new //go:build syntax, common patterns, and how tags interact with the test runner.
- Go Go Tooling: go vet, gofmt, and go test
A practical guide to Go's built-in tooling trio: gofmt for formatting, go vet for static checks, and go test for unit, benchmark, and coverage workflows.
- Go Go Packages and Modules with go.mod
A practical tour of Go packages and modules: go mod init, imports, internal packages, semantic versioning, and the replace directive for local development.
- Go Go Context Cancellation Patterns
Master Go's context package: propagate deadlines, cancel goroutines safely, and avoid leaks with practical patterns for HTTP, database, and pipeline code.