Dependency Management
Overview
Go modules, introduced in Go 1.11 and default since Go 1.16, provide built-in dependency management. This chapter covers everything about go.mod, go.sum, versioning, and managing external libraries.
Understanding Go Modules
A module is a collection of packages with a go.mod file that defines: - Module path (import path) - Go version - Dependencies and their versions
Creating a Module
$ mkdir myproject && cd myproject
$ go mod init github.com/username/myprojectThis creates go.mod:
module github.com/username/myproject
go 1.26
The go.mod File
Anatomy of go.mod
module github.com/username/myproject
go 1.26
require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)
require (
// indirect dependencies
github.com/bytedance/sonic v1.9.1 // indirect
golang.org/x/net v0.10.0 // indirect
)
exclude (
github.com/broken/pkg v1.0.0
)
replace (
github.com/old/module => github.com/new/module v2.0.0
github.com/local/dev => ../local-dev
)
retract (
v1.0.0 // Contains security vulnerability
[v1.1.0, v1.2.0] // Accidental release
)
Directives Explained
| Directive | Purpose |
|---|---|
module |
Module path (required) |
go |
Minimum Go version |
require |
Dependencies with versions |
exclude |
Versions to ignore |
replace |
Substitute modules |
retract |
Mark versions as broken |
The go.sum File
go.sum contains cryptographic checksums representing module dependencies:
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tn+7r7IyQ4PGNmP1c42GGpvQ=
Each entry has: - Module path and version - Hash of module contents (h1:) - Hash of go.mod file
Never edit go.sum manually — it’s managed by Go tools.
Adding Dependencies
Using go get
# Add latest version
$ go get github.com/gin-gonic/gin
# Add specific version
$ go get github.com/gin-gonic/gin@v1.9.1
# Add latest minor version
$ go get github.com/gin-gonic/gin@v1.9
# Add specific commit
$ go get github.com/gin-gonic/gin@a1b2c3d
# Add from branch
$ go get github.com/gin-gonic/gin@mainAutomatic Detection
Simply import and run:
import "github.com/gin-gonic/gin"$ go mod tidy # Downloads and records dependencyUpdating Dependencies
# Update specific package (latest minor/patch)
$ go get -u github.com/gin-gonic/gin
# Update to specific version
$ go get github.com/gin-gonic/gin@v1.10.0
# Update all dependencies
$ go get -u ./...
# Update only patch versions (safer)
$ go get -u=patch ./...Semantic Versioning
Go modules expect Semantic Versioning:
v1.2.3
│ │ │
│ │ └── Patch: bug fixes (backward compatible)
│ └──── Minor: new features (backward compatible)
└────── Major: breaking changes
Major Version Suffixes
For v2+, the module path includes the major version:
// go.mod
require github.com/user/project/v2 v2.1.0
// import
import "github.com/user/project/v2"Managing go.mod
go mod tidy
The most important maintenance command:
$ go mod tidyThis: - Adds missing dependencies - Removes unused dependencies - Updates go.sum
Run this frequently, especially before commits.
go mod download
Pre-download all dependencies:
$ go mod downloadUseful for CI caching or air-gapped environments.
go mod verify
Check that dependencies haven’t been modified:
$ go mod verify
all modules verifiedgo mod why
Explain why a dependency is needed:
$ go mod why golang.org/x/net
# github.com/username/myproject
github.com/username/myproject
github.com/gin-gonic/gin
golang.org/x/net/htmlgo mod graph
Show dependency graph:
$ go mod graph
github.com/username/myproject github.com/gin-gonic/gin@v1.9.1
github.com/gin-gonic/gin@v1.9.1 github.com/bytedance/sonic@v1.9.1
...Vendoring
Copy dependencies into your repository:
# Create vendor directory
$ go mod vendor
# Build using vendor
$ go build -mod=vendorVendoring pros: - Reproducible builds - Works offline - Audit dependencies
Vendoring cons: - Bloated repository - Manual updates
Replace Directive
Local Development
Replace a module with a local path:
replace github.com/some/package => ../local-package
Fork Substitution
Use your fork instead of original:
replace github.com/original/pkg => github.com/yourfork/pkg v1.2.3
Version Override
Force a specific version:
replace github.com/vulnerable/pkg v1.0.0 => github.com/vulnerable/pkg v1.0.1
Private Modules
GOPRIVATE
Configure private repository patterns:
$ go env -w GOPRIVATE=github.com/mycompany/*,gitlab.internal.com/*Git Credentials
For private repos, configure Git:
# Use SSH
$ git config --global url."git@github.com:".insteadOf "https://github.com/"
# Or use access token
$ git config --global url."https://token:${TOKEN}@github.com/".insteadOf "https://github.com/"Dependency Management Best Practices
1. Pin Dependencies
Always use exact versions in production:
# Good: exact version
$ go get github.com/pkg/errors@v0.9.1
# Risky: floating version
$ go get github.com/pkg/errors@latest2. Regular Updates
Check for updates periodically:
$ go list -m -u all3. Minimal Dependencies
Fewer dependencies = fewer problems: - Check if you really need that package - Consider stdlib alternatives - Watch for transitive dependencies
4. Security Scanning
Use vulnerability scanning:
# Built-in (Go 1.18+)
$ go install golang.org/x/vuln/cmd/govulncheck@latest
$ govulncheck ./...Common Issues
Module Not Found
# Ensure GOPROXY is set
$ go env GOPROXY
https://proxy.golang.org,direct
# Try direct fetch
$ GOPROXY=direct go get github.com/some/packageVersion Conflicts
# See what's required
$ go mod graph | grep conflicting-package
# Force specific version
$ go get conflicting-package@v1.2.3Checksum Mismatch
# Clear cache and retry
$ go clean -modcache
$ go mod downloadSummary
| Command | Purpose |
|---|---|
go mod init |
Create new module |
go mod tidy |
Sync dependencies |
go get pkg@version |
Add/update dependency |
go mod download |
Pre-fetch dependencies |
go mod verify |
Verify checksums |
go mod vendor |
Create vendor directory |
More examples
Example: prefer stdlib before go get
Save as main.go and go run . (with go mod init example if needed).
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Service string `json:"service"`
Port int `json:"port"`
}
func main() {
// Many needs are covered by encoding/json, net/http, etc.—no third party.
raw := []byte(`{"service":"api","port":8080}`)
var cfg Config
if err := json.Unmarshal(raw, &cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("%s listens on %d\n", cfg.Service, cfg.Port)
out, _ := json.MarshalIndent(cfg, "", " ")
fmt.Println(string(out))
}Expected:
api listens on 8080
{
"service": "api",
"port": 8080
}
Example: semantic version labels as data
Save as main.go and go run . (with go mod init example if needed).
package main
import (
"fmt"
"strconv"
"strings"
)
// parseSemver is a teaching toy—not a full semver library.
func parseSemver(v string) (major, minor, patch int, err error) {
v = strings.TrimPrefix(v, "v")
parts := strings.Split(v, ".")
if len(parts) != 3 {
return 0, 0, 0, fmt.Errorf("want major.minor.patch, got %q", v)
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}
patch, err = strconv.Atoi(parts[2])
return
}
func main() {
for _, v := range []string{"v1.2.3", "2.0.0", "1.2"} {
maj, min, pat, err := parseSemver(v)
if err != nil {
fmt.Printf("%s -> %v\n", v, err)
continue
}
fmt.Printf("%s -> major=%d minor=%d patch=%d\n", v, maj, min, pat)
}
}Expected:
v1.2.3 -> major=1 minor=2 patch=3
2.0.0 -> major=2 minor=0 patch=0
1.2 -> want major.minor.patch, got "1.2"
Runnable example
Stdlib-only module demo: after go mod init, the program reports module path and dependency list via build info (empty deps until you go get something).
Save as main.go. From an empty directory:
go mod init example.com/demo
go mod tidy
go run .
cat go.modpackage main
import (
"fmt"
"runtime/debug"
)
func main() {
info, ok := debug.ReadBuildInfo()
if !ok {
fmt.Println("no build info (unusual for go run/build)")
return
}
fmt.Println("module:", info.Main.Path)
fmt.Println("go:", info.GoVersion)
if info.Main.Version != "" {
fmt.Println("main version:", info.Main.Version)
}
if len(info.Deps) == 0 {
fmt.Println("deps: (none — stdlib only)")
} else {
fmt.Println("deps:")
for _, d := range info.Deps {
fmt.Printf(" %s %s\n", d.Path, d.Version)
}
}
// Prefer stdlib until you truly need a third-party module.
fmt.Println("tip: run `go get github.com/some/pkg@vX.Y.Z` then `go mod tidy`")
}Expected output (illustrative):
module: example.com/demo
go: go1.22.5
main version: (devel)
deps: (none — stdlib only)
tip: run `go get github.com/some/pkg@vX.Y.Z` then `go mod tidy`
What to notice: - go mod init sets the module path that appears in ReadBuildInfo. - With only stdlib imports, go.mod stays lean—minimal dependencies by design. - go mod tidy is safe to run anytime; it syncs go.mod/go.sum with imports. - Third-party modules only appear under deps after you import and tidy them.
Try next: Add import _ "golang.org/x/time/rate", run go get golang.org/x/time@latest and go mod tidy, then re-run and inspect go.mod / printed deps.