Vendoring #
Dependency management is one of the most evolved aspects of Go’s history. The GOPATH era (before 2018) forced all code into one global directory with a single version per dependency. The dep era (2016-2018) brought semver but was still experimental. The Go Modules era (Go 1.11, 2018) finally provided the mature official solution: every project has a go.mod file explicitly defining its identity and all its dependencies, projects can live in any directory, and versions are locked with cryptographic checksums in go.sum. This article covers Go Modules from the basics to real production scenarios.
A Brief History of Go Dependency Management #
Understanding the history helps explain why Go Modules is designed the way it is:
The GOPATH era (before Go 1.11):
- All code MUST be in $GOPATH/src/
- No versions — only one version per dependency
- go get always fetches the latest version
- Problems: zero reproducibility, "it works on my machine"
The dep era (2016-2018):
- A third-party tool, not official
- Gopkg.toml and Gopkg.lock files
- Semver support, but a complicated setup
- Not integrated with the go tool
The Go Modules era (Go 1.11+, now):
- Official, fully integrated with the go tool
- go.mod and go.sum files at the project root
- Projects can live in any directory
- Explicit versions and reproducible builds
- Module proxy for security and availability
go.mod — The Heart of Every Go Module
#
The go.mod file defines the module’s identity and all its dependencies. Create it with go mod init:
# Create a project directory
mkdir myapp && cd myapp
# Initialize the module
go mod init github.com/username/myapp
The generated go.mod file:
module github.com/username/myapp
go 1.23
After adding some dependencies, go.mod will look like this:
module github.com/username/myapp
go 1.23
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-redis/redis/v9 v9.3.0
go.uber.org/zap v1.26.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
require (
// Indirect dependencies — automatically managed by go mod tidy
github.com/bytedance/sonic v1.10.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
// ... many more
)
The Anatomy of go.mod
#
module — the unique module name, usually following the domain/repo format. This is the prefix used for all packages within this module:
module github.com/username/myapp
↑ domain ↑ repo name
go — the minimum Go version required. Since Go 1.21, this also affects semantic versioning and toolchain selection.
require — the list of direct and indirect dependencies. Indirect dependencies are marked with a // indirect comment.
replace — swaps out the source of a dependency, useful for:
replace (
// Use a local fork for development
github.com/original/pkg => ../local-fork
// Use a fork on GitHub
github.com/original/pkg => github.com/myfork/pkg v1.2.3
// Patch a problematic version
golang.org/x/net v0.17.0 => golang.org/x/net v0.18.0
)
exclude — prevents a specific version from being used (because of a bug or vulnerability):
exclude (
github.com/some/package v1.2.3 // there's a CVE in this version
)
go.sum — Checksums for Supply Chain Security
#
go.sum is a file containing the cryptographic hashes of every module version used:
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPys6V/8s+4yBVwsgWHXFkh4sGPJDW9B6NXTqjh/RuM=
Each entry contains:
- The module name and version
- The SHA-256 hash of the zip of all source files (
h1:...) - The SHA-256 hash of its
go.modalone (/go.mod h1:...)
Go refuses to use a dependency if the hash doesn’t match — this is protection against supply chain attacks like package tampering. Don’t edit go.sum manually — let the go tool manage it.
Commitgo.sumto version control. This ensures all developers and CI/CD use cryptographically identical dependencies. Withoutgo.sumin the repo, there’s no guarantee the downloaded dependency matches the one you used during development.
Managing Dependencies #
Adding a New Dependency #
# Add the latest version
go get github.com/gin-gonic/gin
# Add a specific version
go get github.com/gin-gonic/[email protected]
# Add the latest version from a specific major version
go get github.com/go-redis/redis/v9@latest
# Add a version from a commit hash (rarely used)
go get github.com/pkg/errors@abc123def
# Add a pre-release version
go get github.com/some/[email protected]
Updating Dependencies #
# Update to the latest patch version (v1.9.1 → v1.9.2)
go get github.com/gin-gonic/gin@patch
# Update to the latest minor version (v1.9.x → v1.10.x)
go get github.com/gin-gonic/gin@latest
# Update ALL dependencies to the latest compatible versions
go get -u ./...
# Update only patch versions (safer)
go get -u=patch ./...
Removing a Dependency #
# Remove a dependency from go.mod and go.sum
go mod tidy # automatically removes unused ones
# Or manually: remove the import from code, then run go mod tidy
Semantic Versioning in Go #
Go follows Semantic Versioning (SemVer): MAJOR.MINOR.PATCH:
v1.9.1
│ │ └── PATCH: bug fixes, backward compatible
│ └──── MINOR: new features, backward compatible
└────── MAJOR: breaking changes
Go Modules compatibility rules:
- v0.x.x — no stability guarantees
- v1.x.x — backward compatible for all v1.x.x
- v2.x.x — breaking change, must use a different import path!
Major Versions in the Import Path #
This is an important rule that often confuses: if a module makes a breaking change to v2 or higher, its import path must change by adding /v2:
// Import v1 — no suffix
import "github.com/some/package"
// Import v2 — /v2 in the path is mandatory
import "github.com/some/package/v2"
// Import v3
import "github.com/some/package/v3"
Both versions can be used together in one program — they’re different modules from Go’s point of view:
import (
packagev1 "github.com/some/package"
packagev2 "github.com/some/package/v2"
)
go mod tidy — Tidying Up Dependencies
#
go mod tidy is the most frequently run command during development. It:
- Adds dependencies that are used but not yet in
go.mod - Removes dependencies in
go.modthat aren’t used - Updates
go.sumwith the required checksums
# Run after:
# - Adding new imports in code
# - Removing imports from code
# - Manually changing go.mod
go mod tidy
# Check whether go.mod and go.sum are up to date (for CI)
go mod tidy -diff # Go 1.22+: show the changes without applying them
Vendoring — Storing Dependencies in the Repo #
Vendoring means copying all dependency source code into a vendor/ directory inside the project. This makes the project fully self-contained:
# Create the vendor/ directory from the current go.mod and go.sum
go mod vendor
# Build using vendor/ (Go automatically uses vendor/ if present)
go build ./...
# Build explicitly with vendor
go build -mod=vendor ./...
# Run tests with vendor
go test -mod=vendor ./...
The directory structure after go mod vendor:
myapp/
├── go.mod
├── go.sum
├── main.go
├── vendor/
│ ├── modules.txt ← vendor metadata (don't edit)
│ ├── github.com/
│ │ ├── gin-gonic/
│ │ │ └── gin/ ← gin source code
│ │ └── go-redis/
│ │ └── redis/ ← redis source code
│ └── go.uber.org/
│ └── zap/ ← zap source code
└── internal/
└── ...
When to Use Vendoring? #
USE VENDOR if:
✓ Builds must run without internet access (air-gapped environments)
✓ CI/CD must not download dependencies during builds (security, speed)
✓ You need a full audit of all code that runs
✓ Dependencies from private repos that are hard to access in all environments
✓ Companies with a policy: all code entering production must be reviewed
NO NEED TO VENDOR if:
✗ CI/CD has internet access and good module caching
✗ Using a GOPROXY (local proxy or Athens) that already caches
✗ A small team with consistent environments
✗ The repo is already large and vendor/ would blow up its size
GOPROXY — Module Proxies #
Go downloads modules through GOPROXY — a proxy server that caches modules. The default is proxy.golang.org, managed by Google. The module search and download process can be visualized in the following diagram:
flowchart TD
Start["go get / go mod tidy"] --> LocalCache{"Check Local Cache\n($GOPATH/pkg/mod)?"}
LocalCache -->|"Present"| UseLocal["Use Local Module (Instant)"]
LocalCache -->|"Missing"| CheckPrivate{"Is It a Private Module\n(Matching GOPRIVATE)?"}
CheckPrivate -->|"Yes (Bypass Proxy)"| DirectVCS["Download Directly from VCS\n(GitHub / GitLab / etc.)"]
CheckPrivate -->|"No (Use Proxy)"| FetchProxy["Download via GOPROXY\n(proxy.golang.org)"]
FetchProxy -->|"Fails / direct"| DirectVCS
FetchProxy -->|"Success"| SaveLocal["Save to Local Cache & go.sum"]
DirectVCS -->|"Success"| SaveLocal# View the current GOPROXY configuration
go env GOPROXY
# Output: https://proxy.golang.org,direct
# Format: a comma-separated list of proxy URLs
# "direct" means download straight from VCS (GitHub, etc.) if the proxy fails
Configuration for Private Modules #
If your project uses dependencies from private repositories, you need to configure GONOSUMCHECK and GONOSUMDB:
# Bypass the proxy and sum check for private modules
export GONOSUMDB="gitlab.company.com,github.com/company/*"
export GOPRIVATE="gitlab.company.com,github.com/company/*"
# Or set it in go env
go env -w GOPRIVATE="gitlab.company.com"
go env -w GONOSUMDB="gitlab.company.com"
With GOPRIVATE, Go will:
- Not use GOPROXY for matching modules
- Not verify checksums against the sumdb
- Download directly from VCS
Private Modules with Authentication #
# For GitHub/GitLab private repos over HTTPS
# Create ~/.netrc or a git config
echo "machine github.com login YOUR_TOKEN password x-oauth-basic" >> ~/.netrc
# Or use SSH
git config --global url."[email protected]:".insteadOf "https://github.com/"
Module Workspaces — Working with Multiple Local Modules #
go work (Go 1.18+) lets you work with several modules simultaneously without needing replace in go.mod. Very useful when developing a library and its application together:
# Create a workspace from a directory containing several modules
go work init ./myapp ./mylib ./shared
# Result: go.work
The go.work file:
go 1.23
use (
./myapp // the main module
./mylib // the library being developed
./shared // shared utilities
)
# Add a module to an existing workspace
go work use ./new-module
# Sync the workspace
go work sync
With a workspace, myapp can directly use the latest code from mylib without publishing to GitHub first. Go uses the local version instead of the version in go.mod.
Don’t commitgo.workto the repository (unless the project is truly a monorepo with many modules). This file is local development configuration. Addgo.workandgo.work.sumto.gitignore. Other developers cloning the repo don’t needgo.workto build and test.
The Complete go mod Command Set
#
# Initialize a new module
go mod init [module-name]
# Add/update a dependency
go get package[@version]
# Remove unused dependencies, add missing ones
go mod tidy
# Copy all dependencies into vendor/
go mod vendor
# Verify dependencies haven't changed since download
go mod verify
# Show the dependency graph
go mod graph
# Download all dependencies into the module cache
go mod download
# Edit go.mod programmatically (useful in scripts)
go mod edit -require github.com/pkg/[email protected]
go mod edit -droprequire github.com/old/pkg
go mod edit -replace github.com/orig/pkg=../local-fork
# Show why a package is needed
go mod why github.com/gin-gonic/gin
# List all modules in use
go list -m all
# List available versions
go list -m -versions github.com/gin-gonic/gin
Dependency Management Best Practices #
Choose Dependencies Wisely #
Consider before adding a new dependency:
✓ Is the standard library enough?
(encoding/json, net/http, sync, etc. are very complete)
✓ How big is it?
(a large dependency = a large binary = slower startup)
✓ How actively is it maintained?
(check the last commit date, number of open issues)
✓ How many dependencies does it bring in?
(transitive dependencies all come along)
✓ Is there an alternative with fewer dependencies?
✓ Is its license compatible with your project?
(MIT, Apache 2.0, BSD are usually OK; GPL can be problematic)
Pin Versions, Don’t Use Latest #
// A good go.mod — all versions pinned explicitly
require (
github.com/gin-gonic/gin v1.9.1 // ✓ pinned to a specific version
go.uber.org/zap v1.26.0 // ✓
)
// Avoid using "latest" in the final go.mod
// go get -u ./... is only for intentional updates, not routine
The Standard Development Workflow #
# 1. Start a new project
go mod init github.com/company/myservice
# 2. Add the needed dependencies
go get github.com/gin-gonic/[email protected]
go get go.uber.org/[email protected]
go get gorm.io/[email protected]
# 3. After coding, tidy up dependencies
go mod tidy
# 4. Verify nothing is broken
go mod verify
go build ./...
go test ./...
# 5. For production/CI: create a vendor (optional)
go mod vendor
# 6. Commit all dependency files
git add go.mod go.sum
git add vendor/ # if using vendoring
git commit -m "Add dependencies: gin, zap, gorm"
A Complete Project Workflow Example #
Here’s a real workflow example of building a simple REST API from scratch:
# 1. Create and initialize the module
mkdir todo-api && cd todo-api
go mod init github.com/username/todo-api
# 2. Add dependencies
go get github.com/gin-gonic/[email protected] # HTTP framework
go get gorm.io/[email protected] # ORM
go get gorm.io/driver/[email protected] # SQLite driver
go get go.uber.org/[email protected] # Logging
go get github.com/joho/[email protected] # .env loader
The project structure:
todo-api/
├── go.mod
├── go.sum
├── main.go
├── .env
├── internal/
│ ├── handler/
│ │ └── todo.go
│ ├── model/
│ │ └── todo.go
│ └── repository/
│ └── todo.go
└── vendor/ ← after go mod vendor
The go.mod file after go mod tidy:
module github.com/username/todo-api
go 1.23
require (
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
go.uber.org/zap v1.26.0
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
// ... other transitive dependencies
)
An example main.go using all the dependencies:
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/username/todo-api/internal/handler"
"github.com/username/todo-api/internal/model"
)
func main() {
// Load .env
if err := godotenv.Load(); err != nil {
log.Println(".env file not found, using system environment variables")
}
// Set up the logger
logger, err := zap.NewProduction()
if err != nil {
log.Fatal("Failed to create logger:", err)
}
defer logger.Sync()
// Set up the database
db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
if err != nil {
logger.Fatal("Failed to connect to database", zap.Error(err))
}
// Auto migrate
if err := db.AutoMigrate(&model.Todo{}); err != nil {
logger.Fatal("Failed to migrate database", zap.Error(err))
}
// Set up the router
r := gin.Default()
h := handler.NewTodoHandler(db, logger)
api := r.Group("/api/v1")
{
api.GET("/todos", h.List)
api.POST("/todos", h.Create)
api.GET("/todos/:id", h.Get)
api.PUT("/todos/:id", h.Update)
api.DELETE("/todos/:id", h.Delete)
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
logger.Info("Server is running", zap.String("port", port))
if err := r.Run(":" + port); err != nil {
logger.Fatal("Server failed", zap.Error(err))
}
}
# After coding is done
go mod tidy # tidy up dependencies
go mod verify # verify integrity
go test ./... # run all tests
go build -o bin/todo-api . # build the binary
# For production deployment
go mod vendor # create vendor/
GOOS=linux GOARCH=amd64 go build -mod=vendor -o bin/todo-api-linux .
Summary #
- Go Modules is the official way of dependency management since Go 1.11 — no
depor third-party tools needed.go.moddefines the module’s identity and all direct dependencies;go.sumcontains cryptographic checksums for supply chain security.- Commit both (
go.modandgo.sum) to version control for reproducible builds.go get package@versionadds a dependency;go get -u ./...updates everything.go mod tidyis the mandatory command after changing imports — adds missing ones, removes unused ones.- Major versions v2+ must be in the import path:
github.com/pkg/v2notgithub.com/pkg.go mod vendorcopies all dependencies into thevendor/directory for offline builds and audits.GOPRIVATEconfigures private modules to bypass GOPROXY and GONOSUMDB.go workworks with many local modules simultaneously during development — don’t commit it to the repo.- Choose dependencies wisely — check whether the standard library is enough, look at size and maintenance activity, mind the license.