Skip to main content
Technology & EngineeringGo217 lines

Modules

Go modules, dependency management, versioning, workspaces, and private module configuration

Quick Summary33 lines
You are an expert in Go modules and dependency management, helping developers structure projects, manage dependencies, and handle versioning correctly.

## Key Points

- Always commit both `go.mod` and `go.sum`.
- Run `go mod tidy` before committing to remove unused dependencies and add missing ones.
- Use the `internal/` directory for packages that should not be importable outside your module.
- Pin major dependency upgrades deliberately; review changelogs.
- Use `go mod vendor` if you need hermetic builds or work in an environment without network access.
- Set `GOPRIVATE` for organization-internal modules.
- Use `go.work` for local multi-module development; do not commit `go.work` to shared repos unless the repo is a monorepo.
- **Forgetting `/v2` in import paths**: importing `github.com/foo/bar` when you need `v2` gets the wrong major version.
- **Committing `replace` directives for local paths**: breaks builds for everyone else. Use `go.work` instead.
- **Running `go get` without `./...`**: `go get` in module mode updates `go.mod` but doesn't build; use `go get ./...` to update and check compilation.
- **Not running `go mod tidy`**: `go.mod` may list unused or missing indirect dependencies.
- **Circular module dependencies**: Go does not allow circular imports between packages, and circular module dependencies should also be avoided.

## Quick Example

```bash
mkdir myapp && cd myapp
go mod init github.com/myorg/myapp
```

```bash
# Initialize a workspace
go work init ./app ./lib

# Add a module to the workspace
go work use ./another-module
```
skilldb get go-skills/ModulesFull skill: 217 lines
Paste into your CLAUDE.md or agent config

Modules — Go Programming

You are an expert in Go modules and dependency management, helping developers structure projects, manage dependencies, and handle versioning correctly.

Core Philosophy

Go modules exist to make builds reproducible, dependencies explicit, and version upgrades deliberate. Unlike package managers that resolve "latest compatible" versions at install time, Go uses Minimum Version Selection (MVS): it picks the minimum version that satisfies all requirements, producing the same build everywhere without a lock file. The go.mod file is both the dependency declaration and the version lock, and go.sum provides cryptographic integrity verification. This design means that two developers running go build on the same commit get identical binaries, period.

The module system enforces semantic versioning as a first-class contract. A major version bump (v1 to v2) changes the import path, which means v1 and v2 can coexist in the same binary without conflict. This is not a convention — it is enforced by the toolchain. The consequence is that breaking changes are impossible to introduce accidentally: if you change a public API, you must bump the major version and update every import path. This makes upgrading dependencies an explicit, reviewable decision rather than a surprise.

Good module hygiene centers on a few disciplines: run go mod tidy before every commit to keep dependencies minimal and accurate, use the internal/ directory to prevent downstream packages from importing implementation details, and treat replace directives as temporary development tools that never ship to production. The goal is a go.mod that any developer can read and understand — every dependency justified, every version intentional.

Anti-Patterns

  • Committing replace directives that point to local paths: A replace github.com/foo/bar => ../my-local-fork in go.mod breaks the build for every other developer who does not have that directory. Use go.work for local multi-module development and keep replace out of committed go.mod files.

  • Running go get -u ./... without reviewing changes: Blindly updating all dependencies to their latest versions pulls in untested changes and potential breaking behavior. Update dependencies individually, read changelogs, and run your test suite after each upgrade.

  • Ignoring go.sum or leaving it out of version control: The go.sum file ensures that the exact bytes of every dependency are verified. Without it, builds can silently fetch tampered or different content. Always commit go.sum.

  • Using GOPATH mode or vendoring without understanding the trade-offs: Go modules are the standard since Go 1.16. Falling back to GOPATH or vendoring without a specific need (air-gapped builds, compliance requirements) adds complexity and confuses contributors who expect standard module tooling.

  • Circular module dependencies: While Go prevents circular package imports within a module, it is possible to create circular dependencies between modules (A depends on B, B depends on A). This makes versioning and upgrades painful. Refactor shared code into a third module that both can depend on.

Overview

Go modules (introduced in Go 1.11, default since Go 1.16) are the standard dependency management system. A module is a collection of packages versioned together, defined by a go.mod file at the repository root. The go.sum file provides cryptographic verification of dependencies.

Core Concepts

go.mod

Declares the module path, Go version, and dependencies.

module github.com/myorg/myapp

go 1.22

require (
    github.com/go-chi/chi/v5 v5.0.12
    golang.org/x/sync v0.6.0
)

Semantic Versioning

Go modules enforce semver: vMAJOR.MINOR.PATCH. Major version 2+ requires a /v2 suffix in the module path.

Minimum Version Selection (MVS)

Go selects the minimum version that satisfies all requirements, producing reproducible builds without a lock file.

go.sum

Contains cryptographic hashes of module content for integrity verification. Always commit this file.

Implementation Patterns

Starting a New Module

mkdir myapp && cd myapp
go mod init github.com/myorg/myapp

Common Commands

# Add a dependency (or just import it and run go mod tidy)
go get github.com/go-chi/chi/v5@latest

# Add a specific version
go get github.com/lib/pq@v1.10.9

# Update all dependencies
go get -u ./...

# Update only patch versions
go get -u=patch ./...

# Remove unused dependencies
go mod tidy

# Vendor dependencies
go mod vendor

# Verify dependency integrity
go mod verify

# Show why a dependency is needed
go mod why github.com/some/dep

# Show dependency graph
go mod graph

Project Layout

myapp/
├── go.mod
├── go.sum
├── main.go              # or cmd/myapp/main.go
├── cmd/
│   ├── myapp/
│   │   └── main.go
│   └── mytool/
│       └── main.go
├── internal/            # private packages
│   ├── config/
│   └── database/
├── pkg/                 # public library packages (optional convention)
│   └── api/
├── handler/
├── service/
└── repository/

Multi-Module Workspaces (Go 1.18+)

# Initialize a workspace
go work init ./app ./lib

# Add a module to the workspace
go work use ./another-module

go.work file:

go 1.22

use (
    ./app
    ./lib
)

This lets you develop multiple modules locally without replace directives.

Replace and Exclude Directives

// Use a local fork during development
replace github.com/broken/lib => ../my-fixed-lib

// Pin to a specific commit
replace github.com/some/dep => github.com/some/dep v0.0.0-20240101120000-abcdef123456

// Exclude a known-bad version
exclude github.com/some/dep v1.2.3

Major Version Upgrades

// go.mod for v2
module github.com/myorg/mylib/v2

// Import path includes /v2
import "github.com/myorg/mylib/v2/pkg"

Private Modules

# Tell Go to fetch directly (skip proxy) for private repos
export GONOSUMCHECK="github.com/myorg/*"
export GONOSUMDB="github.com/myorg/*"
export GOPRIVATE="github.com/myorg/*"

# Or configure .netrc / git credential helper for authentication

Tool Dependencies

// tools.go (or tools/tools.go)
//go:build tools

package tools

import (
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
    _ "google.golang.org/protobuf/cmd/protoc-gen-go"
)

Starting with Go 1.24, you can use the tool directive in go.mod instead:

tool (
    github.com/golangci/golangci-lint/cmd/golangci-lint
    google.golang.org/protobuf/cmd/protoc-gen-go
)

Best Practices

  • Always commit both go.mod and go.sum.
  • Run go mod tidy before committing to remove unused dependencies and add missing ones.
  • Use the internal/ directory for packages that should not be importable outside your module.
  • Pin major dependency upgrades deliberately; review changelogs.
  • Use go mod vendor if you need hermetic builds or work in an environment without network access.
  • Set GOPRIVATE for organization-internal modules.
  • Use go.work for local multi-module development; do not commit go.work to shared repos unless the repo is a monorepo.

Common Pitfalls

  • Forgetting /v2 in import paths: importing github.com/foo/bar when you need v2 gets the wrong major version.
  • Committing replace directives for local paths: breaks builds for everyone else. Use go.work instead.
  • Running go get without ./...: go get in module mode updates go.mod but doesn't build; use go get ./... to update and check compilation.
  • Not running go mod tidy: go.mod may list unused or missing indirect dependencies.
  • Circular module dependencies: Go does not allow circular imports between packages, and circular module dependencies should also be avoided.
  • Ignoring go.sum in version control: without it, builds are not verifiable and may fetch tampered content.

Install this skill directly: skilldb add go-skills

Get CLI access →