Go October 9, 2024 Aditya Rawas

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))
}

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.work to version control for library modules. It’s a local development tool. Add go.work to .gitignore and use replace directives (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.

ChangeVersion bumpExample
Bug fixPATCHv1.0.1
New feature (backwards-compatible)MINORv1.1.0
Breaking API changeMAJORv2.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

CommandPurpose
go mod init <path>Initialize a new module
go mod tidyAdd missing, remove unused dependencies
go mod vendorCopy dependencies into vendor/
go mod downloadDownload modules to local cache
go mod graphPrint module dependency graph
go mod verifyVerify downloaded modules match go.sum
go get <module>@<version>Add or upgrade a dependency
go list -m allList all dependencies

Key Takeaways

  • go mod init creates a go.mod file that defines your module and tracks dependencies.
  • Use the replace directive to link local modules during development without publishing.
  • Use Go workspaces (go work) for multi-module development — cleaner than replace and doesn’t pollute go.mod.
  • Run go mod tidy after any dependency change to keep go.mod and go.sum accurate.
  • Vendor with go mod vendor for 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.
Aditya Rawas

Written by

Aditya Rawas

Full-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.