Go Modules & Workspace
Go Modules & Workspace
Go modules (introduced in Go 1.11, default since Go 1.16) are the official dependency management system. A module is a collection of Go packages with a defined module path and versioned dependencies.
go.mod — The Module File
Every module has a go.mod file at its root:
module github.com/yourname/yourproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
golang.org/x/sync v0.6.0
)- module: the module path — used as the import prefix for all packages in the module
- go: minimum Go version this module requires
- require: direct dependencies with minimum versions (Go uses Minimum Version Selection)
go get — Adding Dependencies
go get github.com/some/package@v1.2.3 # specific version
go get github.com/some/package@latest # latest tagged version
go get github.com/some/package@main # specific branchThis updates go.mod and go.sum automatically.
go mod tidy Removes unused dependencies and adds missing ones. Run after editing imports:
go mod tidygo.sum — The Checksum File
go.sum contains cryptographic hashes of every dependency version, ensuring builds are reproducible and tamper-resistant. Commit both go.mod and go.sum to version control.
Semantic Versioning
Go modules use semver (v1.2.3). Major versions ≥ 2 require a path suffix:
import "github.com/user/pkg/v2"This allows v1 and v2 to coexist in the same build.
The replace Directive
Use replace to swap a dependency with a local fork or a different version:
replace github.com/original/pkg => ../local-fork
replace github.com/original/pkg v1.2.3 => github.com/fork/pkg v1.2.4Useful for local development and testing patches before upstreaming.
Multi-Module Workspaces (go.work) Go 1.18 introduced workspaces for working on multiple modules simultaneously without publishing:
go 1.22
use (
./myapp
./mylib
)go work sync syncs the workspace; go work use ./newmodule adds a module.
GOPATH vs Modules
Before modules, all Go code lived under $GOPATH/src. Modules freed Go from GOPATH: you can place projects anywhere on the filesystem. GOPATH is still used for the module cache ($GOPATH/pkg/mod) and installed binaries ($GOPATH/bin).
Vendor Directory
go mod vendor copies all dependencies into a vendor/ directory for offline builds or auditing:
go mod vendor
go build -mod=vendor ./...Code Examples
// This is an annotated go.mod — not valid Go syntax,
// but shows the structure of a real go.mod file.
//
// File: go.mod (lives at the root of your module)
// module declares this module's canonical import path.
// All packages in this repo are imported as:
// github.com/example/myapp/pkg/something
module github.com/example/myapp
// go specifies the minimum Go toolchain version.
// Features above this version will cause compile errors
// on older toolchains.
go 1.22
// require lists direct and indirect dependencies.
// Go uses Minimum Version Selection (MVS): it picks the
// minimum version that satisfies all constraints.
require (
github.com/gin-gonic/gin v1.9.1
golang.org/x/sync v0.6.0
// indirect: required transitively, not directly imported
github.com/bytedance/sonic v1.10.0 // indirect
)
// replace swaps a module for a local fork during development
// replace github.com/original/lib => ../local-lib
// exclude prevents a specific known-bad version from being used
// exclude github.com/bad/pkg v1.0.0go.mod is the module manifest. The module path is the import prefix for all packages in the module. go mod tidy keeps require entries accurate. Commit go.mod and go.sum together.
package main
// After running: go get golang.org/x/text/cases
// go.mod gains: require golang.org/x/text v0.14.0
// go.sum gains: hash entries for the module
// Standard library imports — no module needed
import (
"fmt"
"strings"
)
// Simulating what an external package import looks like
// (golang.org/x/text/cases would provide Unicode-aware casing)
// Without the real package, we demonstrate the pattern:
func titleCase(s string) string {
// Real code would use: cases.Title(language.English).String(s)
words := strings.Fields(s)
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(w[:1]) + strings.ToLower(w[1:])
}
}
return strings.Join(words, " ")
}
func main() {
// go get workflow:
// 1. go get github.com/some/pkg@v1.2.3
// 2. import "github.com/some/pkg" in your code
// 3. go mod tidy (removes unused, adds missing)
// 4. go build ./...
samples := []string{
"hello world",
"the quick brown fox",
"go modules are great",
}
for _, s := range samples {
fmt.Printf("%-30s -> %s\n", s, titleCase(s))
}
}go get updates go.mod and go.sum automatically. go mod tidy removes unused dependencies. Always commit go.sum alongside go.mod for reproducible builds.
package main
import "fmt"
// Demonstrating the replace directive pattern.
//
// Scenario: your app depends on github.com/acme/logger,
// but you need to patch it locally before the fix is merged.
//
// go.mod entries:
//
// require github.com/acme/logger v1.3.0
//
// replace github.com/acme/logger => ../logger-fork
//
// go.work (workspace file) for multi-module development:
//
// go 1.22
// use (
// ./myapp
// ./logger-fork
// )
// // No replace needed — workspace redirects automatically
// Simulating the replaced package behaviour
type Logger interface {
Info(msg string)
Error(msg string)
}
// This would normally come from the replaced module
type PatchedLogger struct{ prefix string }
func (l *PatchedLogger) Info(msg string) {
fmt.Printf("[INFO] %s: %s\n", l.prefix, msg)
}
func (l *PatchedLogger) Error(msg string) {
fmt.Printf("[ERROR] %s: %s\n", l.prefix, msg)
}
func NewLogger(prefix string) Logger {
return &PatchedLogger{prefix: prefix}
}
func main() {
log := NewLogger("app")
log.Info("starting up")
log.Info("loaded config")
log.Error("database connection failed")
fmt.Println()
fmt.Println("Module commands cheat-sheet:")
fmt.Println(" go mod init github.com/you/project")
fmt.Println(" go get github.com/pkg@v1.2.3")
fmt.Println(" go mod tidy")
fmt.Println(" go mod vendor")
fmt.Println(" go work init ./app ./lib")
}The replace directive redirects a module import to a local path or a different remote version. go.work workspaces extend this to multiple modules without modifying go.mod, keeping changes local to the developer.
Quick Quiz
1. What is the purpose of the go.sum file?
2. What does `go mod tidy` do?
3. Why does a major version v2+ module require a path suffix like `/v2`?
4. What is the role of the `replace` directive in go.mod?
Was this lesson helpful?