The module is the unit of sharing
You spent the weekend building a robust CSV parser that handles edge cases the standard library ignores. It lives in a single file inside your personal project. Then you start a second project. You copy the file. Then a third. Suddenly you have three copies of the same code, and fixing a bug means hunting down every copy. You need a way to package that code once and import it everywhere. That's what a Go module is. Publishing a module turns your code into a dependency that other projects can fetch with a single command.
A Go module is a collection of related Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module path and its dependencies. The module path is usually a version control repository URL. When you publish a module, you are making that go.mod and the code reachable by the Go toolchain over the internet. The Go tool uses the module path to find the code, and it uses version tags to pick the right snapshot. You don't need a special registry. Git is the registry. HTTP is the transport. The Go tool talks to Git providers directly.
The module path is the address. Get it right the first time.
Anatomy of a go.mod file
The go.mod file is the manifest for your module. It tells the Go tool what the module is called and what it needs to build. Here's the simplest valid go.mod file for a new library.
// go.mod
module github.com/you/cool-lib
go 1.21
The module line declares the import path for all packages in this tree. Every file in the module will be imported using this path as the prefix. The go line specifies the minimum Go version required to build this module. The tool uses this to enable or disable language features and standard library changes. If a user tries to build this module with Go 1.20, the tool warns them to upgrade.
The module path must be unique across the internet. It is almost always the URL of the version control repository. If your code lives at github.com/you/cool-lib, the module path should be github.com/you/cool-lib. If you set the module path to something else, like cool-lib, the Go tool will fail to resolve the code when others try to import it. The tool fetches the repository at the module path to find the go.mod and the source files. A mismatch breaks the import chain.
Public names in Go start with a capital letter. Private names start with a lowercase letter. This rule applies to packages, functions, types, and variables. If you want a function to be usable by other packages, it must be capitalized. The module path is case-sensitive. github.com/You is different from github.com/you. Stick to lowercase for the path to avoid case-sensitivity nightmares on different filesystems and Git providers.
Run go mod init to create this file. The command takes the module path as an argument.
# Initialize the module with the repository path
go mod init github.com/you/cool-lib
The command creates go.mod in the current directory. It also sets the go directive to the version of the Go tool you are running. You can edit the file manually if needed, but go mod tidy is the standard way to update dependencies later.
Gofmt is mandatory. Run gofmt -w . before you publish. The community expects formatted code. Unformatted code signals a lack of care. Most editors run this on save.
Publishing with tags
The Go tool finds versions by looking at Git tags. You must tag the commit you want to release. The tag must follow semantic versioning and start with a v. Here's the workflow to publish a module.
# Initialize the module with the repository path
go mod init github.com/you/cool-lib
# Add the code and commit it
git add .
git commit -m "Initial release"
# Tag the commit with a semantic version
git tag -a v1.0.0 -m "Release v1.0.0"
# Push code and tags to the remote
git push origin main --tags
The go mod init command creates the manifest. The git tag command marks the commit as a release. The tag name v1.0.0 tells the Go tool this is version 1.0.0. The --tags flag ensures the tag object is pushed to the remote repository. Without it, the tag stays local and others can't see it.
When someone runs go get github.com/you/cool-lib@v1.0.0, the Go tool fetches the tag v1.0.0 from the repository. It downloads the code and updates the importer's go.mod and go.sum files. The module is now available.
Tags start with v. No v, no version.
If you forget the v prefix on the tag, the Go tool won't recognize it as a valid version. You'll see go: github.com/you/cool-lib@v1.0.0: invalid version: unknown revision v1.0.0 when someone tries to fetch it. The tool requires the v to distinguish version tags from other Git tags.
The go.sum file stores cryptographic hashes of every downloaded module. It protects against supply chain attacks. If a module changes on the server, the hash won't match and the build fails. Commit go.sum to your repository. It locks the exact content of dependencies. If you modify go.sum manually or the hash changes unexpectedly, the build fails with go: verifying ...: checksum mismatch: .... The tool protects you from tampering.
Commit go.sum. Trust the hash.
Local development with replace
When developing a module locally, you often want to test it against a consumer project without publishing. The replace directive lets you point to a local path. This is essential for iterative development.
// go.mod
module github.com/you/consumer
go 1.21
require github.com/you/cool-lib v1.0.0
// replace points the dependency to a local directory for testing
replace github.com/you/cool-lib => ../cool-lib
The require line declares the dependency and version. The replace line overrides the fetch location for local development. When the Go tool resolves github.com/you/cool-lib, it looks in ../cool-lib instead of the remote repository. You can edit the library code and rebuild the consumer instantly.
Use go mod edit -replace to add this directive from the command line.
# Add a replace directive pointing to a local path
go mod edit -replace=github.com/you/cool-lib=../cool-lib
The command updates go.mod safely. Remove the replace directive before publishing the consumer project. The replace directive is for development only. It breaks reproducibility if left in production code.
Replace is for local testing. Remove it before shipping.
Major versioning and path changes
Semantic versioning has a twist in Go. Major versions greater than one change the module path. If you release v2.0.0, the module path becomes github.com/you/cool-lib/v2. The v2 is part of the import path. This ensures that code importing v1 and code importing v2 can coexist in the same project without conflict. The Go tool treats them as different modules.
You must update the module line in go.mod and all import paths inside the module to include /v2. This is a breaking change mechanism built into the path.
// go.mod for v2
module github.com/you/cool-lib/v2
go 1.21
The module path now includes /v2. Every file in the module must import other packages using the new path. If you have a package lib inside the module, imports must use github.com/you/cool-lib/v2/lib.
If you forget to update the path for v2, imports break. Code importing github.com/you/cool-lib resolves to v1, but you are looking at v2 code. The compiler complains with undefined: ... if the API changed, or worse, it pulls the wrong version. The path change forces a clean break.
Major versions live in their own path. Update the module line or break the world.
Pitfalls and errors
Publishing a module involves a few common traps. The module path must match the repository URL exactly. If your repo is github.com/you/cool-lib, the module path must be github.com/you/cool-lib. A mismatch means the tool can't find the code. You'll see go: github.com/you/cool-lib: module github.com/you/cool-lib found, but does not contain package ... if the structure is wrong.
Private repositories require authentication. The Go tool uses Git credentials to fetch private modules. If you don't have access, go get fails with go: github.com/you/private-lib@v1.0.0: reading https://... 403 Forbidden. Ensure your Git client is authenticated.
The module proxy caches modules. The default proxy is proxy.golang.org. It speeds up builds and provides a consistent view. You can check the proxy setting with go env GOPROXY.
# Check the current proxy setting
go env GOPROXY
The output shows proxy.golang.org,direct. The tool tries the proxy first, then falls back to direct fetch. You can set GOPROXY=direct to bypass the proxy, but this is rarely needed. The proxy is reliable and fast.
Tags must be pushed. If you tag locally but forget to push, the tag isn't visible. go get fails with go: github.com/you/cool-lib@v1.0.0: invalid version: unknown revision v1.0.0. Always push tags with git push origin --tags.
The module path is the address. Get it right the first time.
Decision matrix
Use a Go module when you want to share code across multiple projects or publish a library for the community. Use a single main.go file when you are prototyping a script that won't be imported anywhere else. Use a private repository when the code contains sensitive logic or internal tools that shouldn't be public. Use go mod init with the repository URL when you are starting a new library or application. Use go get to fetch a published module into your project's dependencies. Use semantic versioning tags starting with v when you release a new version of your module. Use the replace directive when you are testing a local module against a consumer project. Use go mod tidy when you want to clean up unused dependencies and update go.sum. Use gofmt before publishing to ensure your code matches community standards.
Modules are the standard. Stick to the path.