The missing piece in a fresh directory
You create a new folder, write a few lines of Go, and run go run main.go. The compiler immediately rejects the program with no required module provides package .... You haven't written broken code. You just forgot to tell Go where your project lives. Modern Go requires every project to declare itself as a module before the toolchain will compile it or fetch external packages.
What a module actually is
A Go module is a collection of related packages stored in a single directory tree. The module system replaced the old GOPATH workspace model because it makes dependency resolution explicit, reproducible, and independent of where your code sits on disk. Think of a module like a library catalog card. The card lists the book's title, its edition, and every other book it references. Without that card, the librarian cannot verify which books belong together, which editions are compatible, or whether a missing reference actually exists.
The go.mod file is that catalog card. It defines the module path, pins the minimum Go version required to build the code, and records every external dependency along with its exact version. Go uses this file to construct a deterministic dependency graph. Every build that reads the same go.mod and go.sum will download identical code, compile against identical APIs, and produce identical binaries.
The first command
Here's the simplest initialization: spawn a module, declare its path, and let Go create the manifest.
// This is a placeholder for your terminal command, not Go code.
// Run this in your project root to create go.mod
go mod init github.com/yourusername/myproject
The command scans the current directory, creates a go.mod file, and writes two directives. The module directive sets the import path that every package inside this tree will use. The go directive records the version of the Go toolchain you are currently running. You can verify the result by reading the file.
// go.mod contents after initialization
module github.com/yourusername/myproject
go 1.22
The module path matters more than it looks. Go uses this string to locate the code on the module proxy, to generate import statements, and to verify that the directory structure matches the expected repository layout. If you plan to publish this code, the path should match your actual repository URL. If this is a local experiment, a placeholder like example.com/myproject works fine, but you cannot import it from other projects until you host it at that exact URL.
Module paths are the address of your code. Pick one that matches where the code will live.
How the toolchain reads the manifest
When you run go build or go run, the compiler does not search your hard drive randomly. It reads go.mod first, extracts the module path, and maps every import statement to a versioned dependency. If an import starts with your module path, Go looks inside the current directory tree. If an import starts with a different domain, Go contacts the module proxy to fetch the exact version recorded in the manifest.
The proxy does not host your code. It caches releases from version control systems, verifies checksums, and serves versioned archives. This design means you never need to manually download zip files or manage vendor directories. The toolchain handles version selection, conflict resolution, and caching automatically.
You do not need to understand proxy internals to use modules. You only need to know that go.mod is the single source of truth. If the manifest says a package is at version v1.2.3, the compiler will refuse to compile against v1.2.4 unless you explicitly upgrade it. This prevents silent API breaks and keeps your builds deterministic.
Trust the manifest. Let the toolchain resolve the graph.
Adding real dependencies
Initialization only creates an empty manifest. You need dependencies to see the full workflow. Here's how a typical project grows from a bare module to a working application.
// main.go
package main
import (
"fmt"
"log"
"github.com/gin-gonic/gin" // external router package
)
func main() {
// create a new gin engine with default middleware
r := gin.Default()
// register a simple health check endpoint
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// start the server on port 8080
log.Fatal(r.Run(":8080"))
}
Run go get github.com/gin-gonic/gin to fetch the package. The command downloads the latest compatible version, adds a require directive to go.mod, and creates a go.sum file. The sum file contains cryptographic hashes for every downloaded module. Go verifies these hashes on every build to ensure the downloaded code matches exactly what was tested and published.
// go.mod after running go get
module github.com/yourusername/myproject
go 1.22
require github.com/gin-gonic/gin v1.9.1
// go.sum contains verified checksums for every dependency
// do not edit this file manually
// commit it to version control alongside go.mod
After adding or removing packages, run go mod tidy. The command scans your source files, finds every import, removes unused require entries, adds missing ones, and updates go.sum. This keeps the dependency graph clean and prevents stale packages from bloating your build.
Always run go mod tidy before committing. A clean manifest is a reproducible build.
When things go sideways
Module initialization is simple, but a few patterns cause consistent friction.
If you run go mod init in a directory that already contains a go.mod file, the command fails with go.mod already exists. You cannot nest modules or overwrite the manifest accidentally. Delete the file or use go mod edit -module=new/path to rename it.
If your import statements do not match the module directive, the compiler rejects the program with import path ... does not match module path .... Go enforces strict path alignment to prevent ambiguous package resolution. Fix the mismatch by updating the module line or adjusting your import paths.
If you forget to commit go.sum, other developers will see verifying module: checksum mismatch when they run go build. The sum file is not optional. It proves the downloaded code has not been tampered with. Treat it like a lockfile in other ecosystems.
If you are migrating from GOPATH, stop using go env GOPATH to manage projects. The module system ignores the workspace directory entirely. Your code lives wherever you put it, and go.mod tracks everything. The old workspace model is legacy. Modules are the standard.
The worst module bug is a mismatched path that only appears when someone else clones the repo. Align the manifest with reality.
Choosing the right initialization path
Use go mod init when you are starting a new project or converting an unversioned directory into a module. Use go mod edit -module=new/path when you need to rename an existing module without recreating the file. Use go mod tidy after every dependency change to prune unused packages and synchronize the sum file. Use go work init when you are developing multiple local modules simultaneously and need a workspace to override versions during testing. Use plain sequential go get commands when you only need to add a single dependency and trust the toolchain to handle version selection.
Module initialization is a one-time setup. Dependency management is a continuous habit. Keep the manifest clean.