3.7 Go Modules

Go modules are the modern way to manage dependencies in Go. Introduced in Go 1.11 and made the default in Go 1.13, modules have replaced the old GOPATH-based workflow.

Introduction to Modules

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines:

Benefits of modules

Initializing a Module

To start a new Go project with modules:

# Create project directory
mkdir my-project
cd my-project

# Initialize a module
go mod init my-project

This creates a go.mod file:

module my-project

go 1.23

Module path naming

The module path should typically match your repository URL:

# For a GitHub project
go mod init github.com/username/my-project

# For a local project
go mod init my-project

Managing Dependencies

Adding dependencies

When you import a package in your code, Go automatically adds it to your dependencies:

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"  // External dependency
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello"})
    })
    r.Run()
}

Run go mod tidy to download dependencies:

go mod tidy

This will:

go.mod file example

module my-project

go 1.23

require (
    github.com/gin-gonic/gin v1.9.1
)

require (
    github.com/bytedance/sonic v1.8.0 // indirect
    github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
    // ... more dependencies
)

Adding a specific version

go get github.com/gin-gonic/[email protected]

Updating dependencies

# Update all dependencies to latest versions
go get -u ./...

# Update a specific dependency
go get -u github.com/gin-gonic/gin

# Update to a specific version
go get github.com/gin-gonic/[email protected]

Removing unused dependencies

go mod tidy

This removes dependencies that are no longer imported.

Understanding go.mod and go.sum

go.mod file

The go.mod file is the source of truth for your module:

module github.com/user/project

go 1.23

require (
    example.com/other v1.2.3
    example.com/another v0.1.0
)

replace example.com/other => ../local/other

exclude example.com/bad v1.0.0

go.sum file

The go.sum file contains cryptographic checksums of dependencies. While go.mod declares what dependencies you need and their versions, go.sum provides cryptographic verification of those dependencies. It contains checksums (hashes) for every module version your project uses, including transitive dependencies. When you add a dependency, Go automatically updates both files: go.mod records the dependency and version, while go.sum records the checksums. During builds, Go uses go.mod to determine what to download, then uses go.sum to verify the downloaded code matches the expected checksums, preventing tampering and ensuring reproducible builds. Both files should be committed to version control.

github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1S0lVI9J+0mg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:jD2toBW3GJUr7m1owhpKJk7vMnXvXzH1+3U3uBx9r5c=

Important points

Version Resolution and Multiple Versions

When working with Go modules, you may encounter situations where different dependencies require different versions of the same module. Understanding how Go resolves these conflicts is crucial for effective dependency management.

Version conflicts occur because in a complex dependency tree, different packages might require different versions of the same module. For example:

Go must resolve this to a single version that satisfies all requirements.

Go resolves versions using an algorithm called Minimal Version Selection (MVS). This algorithm ensures reproducible builds by selecting the minimum version that satisfies all requirements.

MVS operates in the following steps:

  1. Collect all requirements: Go examines all go.mod files in the dependency tree
  2. Find minimum satisfying version: For each module, it selects the highest version that satisfies all constraints
  3. Single version per module: Only one version of each module is used in the build
  4. Reproducible: The same go.mod and go.sum files always produce the same dependency tree

Example of version resolution

// Your go.mod
module myproject
go 1.23
require (
    github.com/example/lib v1.2.0
    github.com/other/pkg v1.0.0
)

// github.com/example/lib v1.2.0's go.mod requires:
//   github.com/common/util v1.1.0

// github.com/other/pkg v1.0.0's go.mod requires:
//   github.com/common/util v1.0.0

// Result: Go selects v1.1.0 (the minimum that satisfies both)

Viewing the resolved dependency tree

You can see which versions are actually used:

# List all dependencies with versions
go list -m all

# Show why a specific module is included
go mod why -m github.com/example/lib

# Show the dependency graph
go mod graph

Example output of go list -m all

myproject
github.com/example/lib v1.2.0
github.com/other/pkg v1.0.0
github.com/common/util v1.1.0  # Resolved to satisfy both dependencies

Handling version conflicts

If dependencies require incompatible versions (e.g., v1 and v2), Go treats them as different modules:

// These are treated as DIFFERENT modules:
github.com/example/lib v1.2.0
github.com/example/lib/v2 v2.0.0  // Different import path required

Major version changes

When a module releases a major version (v2, v3, etc.), the import path must change:

// v1 module
import "github.com/example/lib"

// v2 module (different import path)
import "github.com/example/lib/v2"

This allows both versions to coexist in the same project.

Practical example

# Initialize a project
go mod init example.com/myapp

# Add a dependency
go get github.com/gin-gonic/[email protected]

# Check what versions are actually used
go list -m all

# See the dependency graph
go mod graph | grep gin

Understanding indirect dependencies

Dependencies can be marked as indirect in go.mod:

require (
    github.com/gin-gonic/gin v1.9.1  // Direct dependency
)

require (
    github.com/bytedance/sonic v1.8.0 // indirect
    // This is a dependency of gin, not directly imported by your code
)

Updating to resolve conflicts

If you need a newer version to satisfy all dependencies:

# Update all dependencies to latest compatible versions
go get -u ./...

# Update a specific dependency
go get -u github.com/example/lib

# Update to a specific version
go get github.com/example/[email protected]

Key points about version resolution

Troubleshooting version issues

# If you get version conflicts, check:
go mod why -m <module-name>  # Why is this module included?
go list -m -versions <module-name>  # What versions are available?
go mod tidy  # Clean up and resolve dependencies

Working with Modules

Vendoring dependencies

You can vendor dependencies (copy them into your project):

go mod vendor

This creates a vendor/ directory with all dependencies. Useful for:

Building with vendor

go build -mod=vendor

Module proxy

Go uses a module proxy by default (proxy.golang.org). You can configure it:

# Check proxy settings
go env GOPROXY

# Use direct mode (no proxy)
export GOPROXY=direct

# Use private proxy
export GOPROXY=https://proxy.company.com,direct

Private modules

For private repositories, configure authentication:

# Configure Git to use credentials
git config --global url."https://user:[email protected]/".insteadOf "https://github.com/"

# Or use GOPRIVATE
go env -w GOPRIVATE=github.com/company/*

Complete Example: A Module-Based Project

Let's create a complete example:

# Create project
mkdir calculator
cd calculator

# Initialize module
go mod init github.com/user/calculator

# Create main.go
cat > main.go << 'EOF'
package main

import (
    "fmt"
    "github.com/user/calculator/math"
)

func main() {
    result := math.Add(10, 5)
    fmt.Printf("10 + 5 = %d\n", result)
}
EOF

# Create math package
mkdir math
cat > math/add.go << 'EOF'
package math

func Add(a, b int) int {
    return a + b
}
EOF

# Build and run
go build
./calculator

Project structure

calculator/
  go.mod
  go.sum
  main.go
  math/
    add.go

This demonstrates: