Creating Your First Go Module: A Step-by-Step Tutorial
Go makes it easy to create reusable, versioned modules that can be imported into any Go project. If you haven’t installed Go yet, start with the Go installation guide. You’ll also want to be familiar with Go data types before writing module code. In this guide, you’ll build a Math Utilities module from scratch, consume it from a separate application, link local modules with the replace directive, use Go workspaces, and learn how to publish your module publicly.
What is a Go Module?
A module is a collection of Go packages versioned together. Every module has a go.mod file at its root that declares:
- The module’s import path (its unique name)
- The Go version it requires
- Its dependencies (other modules it imports)
module example.com/mathutils
go 1.23
require (
github.com/some/dependency v1.2.3
)
Since Go 1.11, modules are the standard way to manage dependencies. You no longer need to put code inside $GOPATH/src.
Step 1: Create the Math Utilities Module
1.1 Create the Module Directory
mkdir mathutils
cd mathutils
1.2 Initialize the Module
go mod init example.com/mathutils
This creates go.mod. The module path (example.com/mathutils) is the import path other code will use. For public modules, this should match your repository URL (e.g., github.com/yourname/mathutils).
1.3 Write the Math Utilities Code
Create math.go inside the mathutils directory:
package mathutils
// Add returns the sum of two integers.
func Add(a, b int) int {
return a + b
}
// Multiply returns the product of two integers.
func Multiply(a, b int) int {
return a * b
}
// Max returns the larger of two integers.
func Max(a, b int) int {
if a > b {
return a
}
return b
}
Exported vs unexported: Functions starting with an uppercase letter (
Add,Multiply) are exported — accessible from other packages. Lowercase functions are unexported (package-private).
Step 2: Call Your Module from Another Project
2.1 Create the Caller Directory
cd ..
mkdir calculator
cd calculator
2.2 Initialize the Caller Module
go mod init example.com/calculator
2.3 Write the Caller Code
Create main.go in the calculator directory:
package main
import (
"fmt"
"example.com/mathutils"
)
func main() {
a, b := 10, 5
fmt.Printf("Add: %d + %d = %d\n", a, b, mathutils.Add(a, b))
fmt.Printf("Multiply: %d × %d = %d\n", a, b, mathutils.Multiply(a, b))
fmt.Printf("Max: max(%d, %d) = %d\n", a, b, mathutils.Max(a, b))
}
Step 3: Link Local Modules with the replace Directive
Since mathutils is on your local machine (not published to a registry), you need to tell the calculator module where to find it.
3.1 Add a replace Directive
go mod edit -replace example.com/mathutils=../mathutils
Your go.mod now contains:
module example.com/calculator
go 1.23
replace example.com/mathutils => ../mathutils
3.2 Sync Dependencies
go mod tidy
This resolves dependencies, adds require entries, and removes unused imports. After running it:
module example.com/calculator
go 1.23
replace example.com/mathutils => ../mathutils
require example.com/mathutils v0.0.0-00010101000000-000000000000
3.3 Run the Application
go run .
Expected output:
Add: 10 + 5 = 15
Multiply: 10 × 5 = 50
Max: max(10, 5) = 10
Step 4: Go Workspaces (The Modern Alternative)
The replace directive works but is awkward — you have to update go.mod every time you move directories, and you must remember to remove it before publishing.
Go workspaces (introduced in Go 1.18) solve this more elegantly. A workspace lets you work with multiple modules simultaneously without modifying their go.mod files.
4.1 Create a Workspace
Navigate to the parent directory containing both modules:
cd .. # You should now be in the folder containing both mathutils/ and calculator/
go work init ./mathutils ./calculator
This creates a go.work file:
go 1.23
use (
./calculator
./mathutils
)
4.2 Run Without replace
Now delete (or don’t add) the replace directive from calculator/go.mod. The workspace handles the local resolution automatically:
cd calculator
go run .
It still works — Go reads go.work and routes example.com/mathutils to your local ./mathutils directory.
4.3 Add More Modules to the Workspace
go work use ./another-module
Important: Don’t commit
go.workto version control for library modules. It’s a local development tool. Addgo.workto.gitignoreand usereplacedirectives (or publish the module) for CI/CD.
Step 5: Vendoring Dependencies
For reproducible builds in CI/CD pipelines, you can vendor all dependencies — copy them into a vendor/ directory in your project:
go mod vendor
This creates:
calculator/
├── go.mod
├── go.sum
├── main.go
└── vendor/
├── example.com/mathutils/
│ └── math.go
└── modules.txt
Build using the vendor directory:
go build -mod=vendor ./...
Vendoring ensures builds work even if the module proxy or VCS is unavailable.
Step 6: Publishing Your Module
When your module is ready to share, publish it by pushing to a public Git repository. Go’s module system uses VCS tags for versioning.
6.1 Use a Public Repository Path
Change your module path to match your repository:
go mod init github.com/yourusername/mathutils
6.2 Tag a Version
git init
git add .
git commit -m "Initial release"
git tag v1.0.0
git push origin main --tags
Go uses semantic versioning (SemVer): vMAJOR.MINOR.PATCH.
| Change | Version bump | Example |
|---|---|---|
| Bug fix | PATCH | v1.0.1 |
| New feature (backwards-compatible) | MINOR | v1.1.0 |
| Breaking API change | MAJOR | v2.0.0 |
Important: For v2+, the module path must include the major version:
github.com/yourusername/mathutils/v2. This lets consumers pin to v1 or v2 independently.
6.3 Consumers Install It
Once pushed and tagged, anyone can install your module:
go get github.com/yourusername/mathutils@v1.0.0
It will appear on pkg.go.dev automatically within a few minutes — Go’s module proxy indexes public GitHub repositories.
go.sum: The Integrity File
Alongside go.mod, Go maintains a go.sum file with cryptographic hashes of every dependency:
github.com/some/dep v1.2.3 h1:abc123...
github.com/some/dep v1.2.3/go.mod h1:def456...
This ensures that the exact same code is used every time — even if the upstream repository changes a tag. Always commit go.sum to version control.
Common go mod Commands
| Command | Purpose |
|---|---|
go mod init <path> | Initialize a new module |
go mod tidy | Add missing, remove unused dependencies |
go mod vendor | Copy dependencies into vendor/ |
go mod download | Download modules to local cache |
go mod graph | Print module dependency graph |
go mod verify | Verify downloaded modules match go.sum |
go get <module>@<version> | Add or upgrade a dependency |
go list -m all | List all dependencies |
Key Takeaways
go mod initcreates ago.modfile that defines your module and tracks dependencies.- Use the
replacedirective to link local modules during development without publishing. - Use Go workspaces (
go work) for multi-module development — cleaner thanreplaceand doesn’t pollutego.mod. - Run
go mod tidyafter any dependency change to keepgo.modandgo.sumaccurate. - Vendor with
go mod vendorfor reproducible CI/CD builds without network access. - Publish by pushing to a public Git repo with a SemVer tag — pkg.go.dev indexes it automatically.
- For breaking changes (v2+), append the major version to the module path.
Related Articles
Understanding Structs in Go: The Foundation of Data Modeling
Learn how Go structs work — defining custom types, creating instances, passing by value vs pointer, adding methods, constructor functions, struct tags for JSON, anonymous structs, and struct embedding.
GoDemystifying Go's fmt.Sprintf: A Practical Guide
Master Go's fmt.Sprintf() with format verbs (%v, %s, %d, %q, %b, %x), width and padding, zero-padding, argument indexing, fmt.Fprintf for HTTP responses, fmt.Sscanf for parsing, and common pitfall fixes.
GoGo Data Types: A Comprehensive Guide for Developers
Explore every Go data type — bool, string, int, float, byte, rune, arrays, slices, maps, pointers, and interfaces — with practical examples, type conversion, and best practices for Go beginners.
Written by
Aditya RawasFull-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.