Create and publish module

Initialize a module, tag a version in git, and push the tag to publish your Go module.

You built something useful. Now share it.

You wrote a parser that handles a tricky CSV format. It works perfectly in your project. A colleague asks for it. You copy the file. They paste it. A week later, you fix a bug. You email the updated file. They paste it again. They forget to update. The bug returns. Copy-pasting code is fragile. It breaks the build when dependencies drift. It hides version history.

Go solves this with modules. A module turns your code into a dependency that others can download, version, and verify with a single command. Publishing a module is not a complex ritual. It is a sequence of three steps: initialize the manifest, tag a version in git, and push the tag. The Go toolchain and the module proxy handle the rest.

What a module actually is

A module is a collection of packages stored in a version control system. It is defined by a go.mod file at the root of the repository. This file is the manifest. It declares the module path, the minimum Go version, and the dependencies the module requires.

Think of a module like a shipping container. The container has a unique label (the module path). It holds cargo (the packages). It has a batch number (the version). The warehouse (the module proxy) stores containers and ships them to anyone who requests the label and batch number.

The module path is the import path. It usually matches the repository URL. If your code lives at github.com/user/mylib, the module path should be github.com/user/mylib. The path must be unique across the internet. Use a domain you control. If you use GitHub, the path is github.com/username/repo. If you use GitLab, it is gitlab.com/username/repo.

Versions are git tags. The proxy reads tags to determine which code corresponds to which version. A tag named v1.0.0 maps to version v1.0.0. The tag must follow semantic versioning. Major versions matter. If you break the API, you bump the major version. Version 2 changes the import path to github.com/user/mylib/v2. This prevents accidental breakage in downstream projects.

Initialize the manifest

Start by creating the go.mod file. The go mod init command generates this file. It writes the module path and the Go version. Run this command in the root directory of your repository.

// Terminal session
// Initialize the module with the import path matching your repo.
// The path must be unique and stable.
go mod init github.com/user/mylib

// The command creates go.mod.
// It does not touch git. You still need to commit the file.

The go.mod file looks like this:

// go.mod
// module declares the import path for this module.
// All packages inside use this prefix for imports.
module github.com/user/mylib

// go sets the minimum Go version required to build this module.
// The toolchain enforces this version for compatibility checks.
go 1.21

The module line is the address. Every package in the module inherits this prefix. If you have a package in a subdirectory pkg/parser, the import path becomes github.com/user/mylib/pkg/parser. The go line sets the baseline. If you use features from Go 1.21, the proxy ensures consumers have at least that version.

Commit the go.mod file immediately. The module does not exist until the manifest is in version control.

// Terminal session
// Add the manifest and commit it.
// The module path is now recorded in history.
git add go.mod
git commit -m "Initialize module github.com/user/mylib"

Write the code and verify

Add your packages. Follow standard Go conventions. Run gofmt on every file. The community expects consistent formatting. Most editors run gofmt on save. If you argue about indentation, you are fighting the tool. Let the tool decide.

Write doc comments for exported names. Start the sentence with the name of the function or type. This is the convention for godoc and IDE tooltips.

// pkg/clamp/clamp.go
package clamp

// Clamp returns val clamped to the range [min, max].
// It returns min if val is less than min.
// It returns max if val is greater than max.
func Clamp(val, min, max int) int {
    // Check lower bound first.
    if val < min {
        return min
    }

    // Check upper bound.
    if val > max {
        return max
    }

    // Value is within range.
    return val
}

Test the module locally. Run go test ./.... Fix failures. The module should build and pass tests before you publish.

// pkg/clamp/clamp_test.go
package clamp

import "testing"

// TestClamp verifies the bounds logic.
func TestClamp(t *testing.T) {
    // Test value below minimum.
    if got := Clamp(0, 5, 10); got != 5 {
        t.Errorf("Clamp(0, 5, 10) = %d; want 5", got)
    }

    // Test value above maximum.
    if got := Clamp(15, 5, 10); got != 10 {
        t.Errorf("Clamp(15, 5, 10) = %d; want 10", got)
    }
}

Commit the code. The repository now contains the manifest and the packages.

Tag and push the version

Publishing is tagging and pushing. The proxy does not read branches. It reads tags. Create an annotated tag with a semantic version number. The tag name must start with v.

// Terminal session
// Tag the current commit as version 1.0.0.
// Use an annotated tag to include metadata.
git tag -a v1.0.0 -m "Release v1.0.0"

// Push the tag to the remote repository.
// The proxy fetches tags from the remote.
git push origin v1.0.0

The tag creates the version. Once the tag is pushed, the module is published. The proxy caches the module shortly after the tag appears. Consumers can download it using go get.

// Terminal session in a consumer project
// Fetch the module and add it to go.mod.
// The toolchain queries the proxy for the tag.
go get github.com/user/mylib@v1.0.0

The consumer's go.mod file updates automatically. The go.sum file records the checksum. The go.sum file is a lock file. It ensures reproducibility. Do not edit go.sum manually. The toolchain generates it.

How the proxy works

The module proxy is a service that distributes modules. The default proxy is proxy.golang.org. It caches modules from git repositories. It verifies signatures if available. It provides a consistent interface for fetching code.

When a consumer runs go get, the toolchain asks the proxy for the module. The proxy checks its cache. If the module is missing, the proxy fetches the tag from the git repository. It downloads the code. It computes the checksum. It stores the result. The proxy then serves the module to the consumer.

This design has benefits. The proxy shields consumers from git outages. It provides fast downloads via edge caches. It verifies content integrity. If the checksum in go.sum does not match the proxy's checksum, the build fails. This prevents tampering.

The proxy trusts git tags. If you delete a tag, the proxy may still serve the cached version. If you force-push a commit that changes the tag's content, the proxy detects the mismatch and rejects the version. Tags are immutable in the eyes of the proxy. Treat them as such.

Pitfalls and errors

Publishing fails for predictable reasons. The compiler and toolchain report clear errors.

If you forget to push the tag, the proxy cannot find the version. The consumer sees an error like module github.com/user/mylib@v1.0.0: reading https://proxy.golang.org/...: 404 Not Found. The proxy returns 404 because the tag does not exist in the remote repository. Push the tag.

If the module path in go.mod does not match the repository path, imports break. The consumer tries to import github.com/user/mylib. The proxy looks for a tag on that repo. If the go.mod inside declares a different path, the proxy rejects the module. The error looks like module path mismatch: go.mod declares module path "example.com/wrong" but the module is being accessed as "github.com/user/mylib". Fix the go.mod file. The path must match the repo URL.

If you use a private repository, the public proxy cannot fetch it. The proxy returns 404. Private modules require a private proxy or direct git access. Configure GOPRIVATE to tell the toolchain to bypass the proxy for specific paths.

// Terminal session
// Configure GOPRIVATE for internal repos.
// The toolchain fetches these directly from git.
export GOPRIVATE=github.com/mycompany/*

If you change the major version, you must update the import path. Version 2 requires the path github.com/user/mylib/v2. The go.mod file must declare module github.com/user/mylib/v2. All imports inside the module must use the /v2 suffix. If you forget, the toolchain complains with module declares its path as "github.com/user/mylib" but was required as "github.com/user/mylib/v2". Bump the major version carefully. Update the path everywhere.

Decision matrix

Choose the right strategy for your code.

Use a published public module when you want the community to use your code. Publish when the API is stable and the tests pass. Push the tag to make the version available.

Use a replace directive when you are developing a module locally and need to test it in a consumer project. Add replace github.com/user/mylib => ../mylib to the consumer's go.mod. Remove the directive before publishing the consumer.

Use a private module when the code contains internal logic or secrets. Configure GOPRIVATE in your CI environment. Use a private proxy if your team needs caching and audit logs.

Use a monorepo with multiple modules when different teams need independent versioning. Create a go.mod in each subdirectory. Tag releases for each module separately. This allows teams to ship updates without coordinating on a single version number.

Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing. Modules add overhead. If the code is a single script for personal use, skip the module. Just run go run main.go.

Where to go next

Tags are versions. Push the tag, or the world doesn't see it. The proxy reads tags. Trust the tag.