Skip to content

Go Modules & Dependencies Intermediate

Introduction

Go Modules (introduced in Go 1.11, default since Go 1.16) is Go's official dependency management system. It replaces the old GOPATH-based workflow with versioned, reproducible builds. Every Go project is a module defined by a go.mod file. Go's approach to versioning is unique: it uses Minimum Version Selection (MVS) instead of the "latest compatible" strategy used by npm, pip, and Cargo. Understanding modules, semantic versioning, and the go.sum lockfile is essential for any Go developer.


Syntax & Usage

Creating a Module

mkdir myproject && cd myproject
go mod init github.com/user/myproject

This creates a go.mod file:

module github.com/user/myproject

go 1.22

go.mod File Structure

module github.com/user/myproject

go 1.22

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.6.0
)

require (
    // indirect dependencies (required by your direct dependencies)
    github.com/bytedance/sonic v1.10.2 // indirect
    golang.org/x/net v0.20.0 // indirect
)

replace github.com/broken/pkg => github.com/fixed/pkg v1.2.0

retract v1.0.0 // published with critical bug
Directive Purpose
module Module path (import path for your module)
go Minimum Go version required
require Direct and indirect dependencies with versions
replace Override module source (local path or alternate module)
exclude Skip specific versions
retract Mark versions of your own module as broken

go.sum — The Integrity Lockfile

go.sum contains cryptographic hashes of every dependency (and its go.mod). It ensures reproducible builds and detects tampering.

github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKFbGRn7nbIqu9HhEDnDfBIJo=

Always Commit go.sum

go.sum must be committed to version control. It's your guarantee that every developer and CI system builds with exactly the same dependency bytes.


Essential go mod Commands

go get github.com/lib/pq@v1.10.9   # add/update a dependency
go get github.com/lib/pq@latest     # get latest version
go get github.com/lib/pq@none       # remove a dependency

go mod tidy                          # add missing, remove unused deps
go mod download                      # download deps to local cache
go mod verify                        # verify go.sum hashes match
go mod vendor                        # copy deps into vendor/ directory
go mod graph                         # print dependency graph
go mod why github.com/lib/pq        # explain why a dependency is needed

go get vs go install

go get manages dependencies in your module's go.mod. go install builds and installs a binary. Since Go 1.17, go get no longer builds binaries — use go install pkg@version instead.


Semantic Versioning in Go

Go enforces Semantic Versioning strictly:

v1.2.3
│ │ │
│ │ └── Patch: bug fixes (backward compatible)
│ └──── Minor: new features (backward compatible)
└────── Major: breaking changes
go get github.com/lib/pq@v1.10.9       # exact version
go get github.com/lib/pq@v1.10         # latest patch in v1.10.x
go get github.com/lib/pq@latest        # latest stable
go get github.com/lib/pq@master        # branch name
go get github.com/lib/pq@abc1234       # commit hash (pseudo-version)

Major Version Suffixes

Breaking change? The import path must change.

import "github.com/user/pkg"       // v0.x or v1.x
import "github.com/user/pkg/v2"    // v2.x
import "github.com/user/pkg/v3"    // v3.x
// go.mod for v2
module github.com/user/pkg/v2

go 1.22

Major Version = New Import Path

This is Go's import compatibility rule: if an old import path works, it must remain backward compatible. Breaking changes require a new major version suffix in the module path. v0.x and v1.x share the same path; v2+ must use /v2, /v3, etc.


Minimum Version Selection (MVS)

Go's dependency resolution algorithm is fundamentally different from other package managers.

Your module requires:
  - pkg A v1.2.0
  - pkg B v1.1.0

pkg A requires:
  - pkg C v1.3.0

pkg B requires:
  - pkg C v1.5.0

MVS selects: pkg C v1.5.0 (the MINIMUM version that satisfies all constraints)
Other Package Managers Go (MVS)
Select latest compatible version Select minimum version satisfying all constraints
Lockfile needed to freeze versions go.mod IS the source of truth
Resolver can have conflicts No conflicts possible — always picks the minimum
Non-deterministic without lockfile Deterministic by design

MVS means that adding a dependency to your project never downgrades an existing dependency and the build is reproducible without a separate lockfile.


replace Directive

Override where Go fetches a module — useful for local development, forks, and patching.

// Use a local copy during development
replace github.com/user/pkg => ../pkg

// Use a fork
replace github.com/original/pkg => github.com/fork/pkg v1.2.3

// Pin to specific commit
replace github.com/user/pkg => github.com/user/pkg v0.0.0-20240101000000-abcdef123456

replace Is Not Transitive

replace directives only apply in the main module (the one you're building). If module A replaces module C, but you depend on module A, the replace does NOT affect your build.


retract Directive

Mark versions of your own module as broken or accidentally published.

// go.mod of your module
retract (
    v1.0.0 // accidental publish with broken API
    [v1.1.0, v1.2.0] // range of broken versions
)

Retracted versions still exist but go get @latest skips them and go list warns about them.


Vendoring

go mod vendor          # copies all dependencies into vendor/
go build -mod=vendor   # build using vendored deps (not module cache)

The vendor/ directory is useful for:

  • Air-gapped builds (no network access)
  • Auditing dependencies (all code in your repo)
  • Faster CI (no download step)

Since Go 1.14, go mod vendor is well-supported. The -mod=vendor flag is auto-detected if a vendor/ directory exists.


Module Proxies (GOPROXY)

# Default proxy chain (most Go setups)
GOPROXY=https://proxy.golang.org,direct

# Common configurations
GOPROXY=https://proxy.golang.org,direct        # default: Google's proxy, then direct
GOPROXY=off                                     # no network access
GOPROXY=direct                                  # always fetch from source
GOPROXY=https://goproxy.io,https://proxy.golang.org,direct  # multiple proxies
Variable Purpose
GOPROXY Comma-separated list of module proxies
GONOSUMDB Skip checksum verification for matching modules
GONOSUMCHECK Skip checksum database for matching modules
GOPRIVATE Modules matching this pattern skip proxy AND sumdb

Private Modules (GOPRIVATE)

# Tell Go not to use proxy/sumdb for your private repos
GOPRIVATE=github.com/mycompany/*,gitlab.mycompany.com/*

# Or set individually
GONOSUMDB=github.com/mycompany/*
GONOPROXY=github.com/mycompany/*

Workspaces (go.work) — Multi-Module Development

Go 1.18+ workspaces let you work on multiple related modules simultaneously without replace directives.

# Create a workspace
go work init ./api ./shared ./worker
// go.work
go 1.22

use (
    ./api
    ./shared
    ./worker
)
project/
├── go.work
├── api/
│   ├── go.mod      (module github.com/user/api)
│   └── main.go
├── shared/
│   ├── go.mod      (module github.com/user/shared)
│   └── utils.go
└── worker/
    ├── go.mod      (module github.com/user/worker)
    └── main.go

Changes in shared/ are immediately visible to api/ and worker/ without publishing or replace directives.

Don't Commit go.work

go.work is typically for local development only. Add it to .gitignore. Each module should work independently with its own go.mod.


Quick Reference

Command / Concept Usage Notes
Initialize module go mod init path Creates go.mod
Add dependency go get pkg@version Updates go.mod and go.sum
Remove unused go mod tidy Cleans up go.mod and go.sum
Vendor deps go mod vendor Copies to vendor/
Verify integrity go mod verify Checks against go.sum
Dependency graph go mod graph Text-based dep tree
Major version import "pkg/v2" Path changes at v2+
Replace replace old => new Local dev, forks
Retract retract v1.0.0 Mark your version as broken
Workspace go work init ./a ./b Multi-module local dev
Proxy GOPROXY=proxy,direct Module download sources
Private GOPRIVATE=pattern Skip proxy and sumdb

Best Practices

  1. Run go mod tidy before committing — removes unused dependencies and adds missing ones
  2. Always commit go.sum — it's your build integrity guarantee
  3. Use semantic versioning for your modules — it's enforced by the ecosystem
  4. Set GOPRIVATE for internal/corporate modules — avoids proxy leaks and sumdb errors
  5. Prefer go mod tidy over manual go get for cleanup — it's idempotent and thorough
  6. Use workspaces for multi-module repos instead of replace directives
  7. Don't commit go.work — it's a local development convenience
  8. Vendor for reproducibility in CI/CD if your environment lacks reliable network access
  9. Use retract when you accidentally publish a broken version — don't delete tags

Common Pitfalls

Forgetting the /v2 Suffix

// WRONG -- importing v2 without path suffix
import "github.com/user/pkg" // this gets v1!

// RIGHT
import "github.com/user/pkg/v2"
The module path in go.mod must also include /v2. Both the import path and module declaration must match.

Using replace for Published Modules

// go.mod
replace github.com/lib/pq => ../my-local-pq
replace is great for local development but don't commit it for published modules. It only applies to the main module, so downstream consumers won't see it.

Not Running go mod tidy

Manually editing go.mod can leave it in an inconsistent state — missing indirect dependencies or stale entries. Always run go mod tidy after making changes.

GOPRIVATE Not Set for Internal Repos

go get github.com/mycompany/internal-lib
# Error: 410 Gone (proxy can't access private repo)
Set GOPRIVATE=github.com/mycompany/* in your environment or CI configuration.

Pseudo-Versions Pinned to Commits

require github.com/user/pkg v0.0.0-20240115120000-abc123def456
Pseudo-versions are valid but fragile. Prefer tagged releases. If a dependency doesn't tag releases, consider whether it's stable enough to use.


Performance Considerations

Scenario Recommendation
Slow CI builds Use go mod vendor + -mod=vendor to avoid downloads
Module proxy latency Use a closer proxy (GOPROXY=https://goproxy.io,...) or run Athens locally
Large dependency tree go mod graph to audit; remove unused deps with go mod tidy
Docker builds Copy go.mod and go.sum first, run go mod download, then copy source — leverages Docker layer caching
Monorepo Use workspaces (go.work) to avoid multiple replace directives
Download cache GOMODCACHE (default $GOPATH/pkg/mod) is shared across all projects

Interview Tips

Interview Tip

"What is Minimum Version Selection?" MVS is Go's dependency resolution algorithm. Unlike npm or pip that select the latest compatible version, Go selects the minimum version that satisfies all constraints. This makes resolution deterministic, reproducible, and conflict-free — no need for a separate lockfile beyond go.mod.

Interview Tip

"What's the difference between go.mod and go.sum?" go.mod declares your module's path, Go version, and dependency requirements. go.sum stores cryptographic checksums of every dependency to ensure integrity. Both should be committed, but go.sum is a verification file, not a lockfile — MVS makes go.mod itself deterministic.

Interview Tip

"What happens at v2?" Go's import compatibility rule requires that breaking changes change the import path. At v2+, the module path must include a /v2 suffix (e.g., github.com/user/pkg/v2), and all imports must use this path. This allows v1 and v2 to coexist in the same build.

Interview Tip

"How do you handle private modules?" Set the GOPRIVATE environment variable to match your private module paths. This tells Go to skip the public proxy (proxy.golang.org) and checksum database (sum.golang.org) for those modules, fetching directly from the source.

Interview Tip

"What are Go workspaces?" Introduced in Go 1.18, go.work files let you develop multiple modules simultaneously in a single workspace. Changes to one module are immediately visible to others without replace directives or publishing. Ideal for monorepos or cross-module development.


Key Takeaways

  • Every Go project is a module defined by go.mod — no more GOPATH workflow
  • go.sum ensures build integrity with cryptographic checksums — always commit it
  • Go uses Minimum Version Selection (MVS) — deterministic, conflict-free resolution
  • go mod tidy is your best friend — run it before every commit
  • Major versions (v2+) require a path suffix (/v2) in both module path and imports
  • replace is for local development; retract marks your own broken versions
  • GOPRIVATE is essential for private/corporate modules — prevents proxy and sumdb errors
  • Workspaces (go.work) simplify multi-module development without replace hacks
  • Vendoring (go mod vendor) provides offline, reproducible builds for CI/CD