Skip to content
C Codeloom
Go

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.

·4 min read · By Codeloom
Intermediate 9 min read

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
)
Module anatomy

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 module line must match the import path consumers use. Renaming a repository without updating go.mod breaks 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 get outside a module: in module-aware mode, go get modifies go.mod. To install a binary tool globally, use go install example.com/tool@latest.
  • Replace without commit: a replace directive pointing to a local path is great for development but should not be committed to main. 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.