Question
In Go modules, go mod init and go build create go.mod and go.sum files and resolve dependency versions automatically.
If a module has no tagged releases, Go may use the latest commit. If a module does have releases, Go usually selects an appropriate tagged version.
What should I do when I need functionality from a commit that was made after the latest published release?
How can I make go.mod depend on a specific commit instead of a released version?
For example, is it valid to write something like this manually in go.mod?
module /my/module
require (
github.com/someone/some_module v0.0.0-20181121201909-af044c0995fe
)
In this kind of version string:
- does
v0.0.0represent a base version, - does
20181121201909represent the commit timestamp, - and does
af044c0995ferepresent the commit hash?
Should this information be looked up and entered manually, or is there a better and more idiomatic way to do it in Go?
Short Answer
By the end of this page, you will understand how Go modules refer to a dependency at a specific Git commit, what a pseudo-version is, when Go generates it for you, and the safest way to add or update such a dependency without editing version strings by hand.
Concept
Go modules normally prefer tagged versions such as v1.2.3 because tagged releases are stable, reproducible, and easy to understand. But sometimes you need code that exists only in a repository commit that has not been released yet.
In that case, Go uses a pseudo-version.
A pseudo-version is a version string that lets Go treat an untagged commit like a normal module version. It typically looks like this:
v0.0.0-20181121201909-af044c0995fe
or, if it is based on an existing tagged release, something like:
v1.2.3-0.20240102150405-abcdef123456
A pseudo-version includes:
- a base semantic version
- a timestamp
- a commit hash prefix
This matters because Go needs every dependency to have a version-like identifier, even when the source is just a commit.
## Why this matters
Using a specific commit is useful when:
- a bug fix exists in the repository but has not been released
- you need to test an upstream change before a release is cut
- you depend on a private module where tagging is infrequent
- you want a reproducible build tied to a known commit
In real projects, you usually **do not write pseudo-versions by hand**. Instead, you ask the Go tool to resolve a commit, branch, or `@latest`, and it updates `go.mod` with the correct pseudo-version automatically.
Mental Model
Think of a Go module version like a package label in a warehouse.
- A tagged release such as
v1.4.0is a package with a printed product label. - A raw commit is like an item that exists in the warehouse but has not been officially boxed and labeled yet.
- A pseudo-version is the temporary shipping label Go creates so that the item can still be tracked reliably.
So when you depend on a commit, Go does not leave it as a loose Git hash. It turns that commit into a structured version string so the dependency system can sort, compare, and reproduce builds consistently.
Syntax and Examples
The most common and idiomatic way is to use go get with a commit hash, branch name, or version.
Use a specific commit
go get github.com/someone/some_module@af044c0995fe
Go will resolve that commit and write the correct pseudo-version into go.mod.
Example result:
require github.com/someone/some_module v0.0.0-20181121201909-af044c0995fe
Use a branch
go get github.com/someone/some_module@main
If main points to an unreleased commit, Go will again generate a pseudo-version.
Use the latest available version
go get github.com/someone/some_module@latest
This usually picks the latest tagged release if one exists.
What the pseudo-version parts mean
For:
v0.0.0-20181121201909-af044c0995fe
The parts are roughly:
Step by Step Execution
Consider this command:
go get github.com/example/lib@a1b2c3d4e5f6
Assume commit a1b2c3d4e5f6 exists in that repository.
What happens step by step
-
The Go tool looks up the module path:
github.com/example/lib -
It resolves the revision:
a1b2c3d4e5f6 -
It checks the repository metadata, including:
- the exact commit time
- whether there are relevant tags before that commit
- the correct major version path rules
-
It generates a valid pseudo-version, for example:
v1.4.2-0.20240201112233-a1b2c3d4e5f6 -
It writes that version into
go.mod. -
It records cryptographic checksums in
go.sum. -
Future builds can reproduce the same dependency version exactly.
Small trace example
Suppose your go.mod starts as:
Real World Use Cases
Using a specific commit is common in practical Go development.
1. Hotfix not released yet
A library maintainer merges a bug fix, but no new tag has been created. You can temporarily depend on that commit.
2. Testing an upstream feature
You want to try a feature before deciding whether to adopt it in production.
3. Internal or private modules
Your company may use private repositories where developers commit often but tag releases less often.
4. Reproducible CI builds
A commit-based dependency lets CI and local development use the exact same source state.
5. Waiting for an official release
You may pin to a commit for a short time, then switch back to a normal tagged version when the release is published.
Real Codebase Usage
In real projects, developers usually avoid hand-editing pseudo-versions and rely on Go commands to keep module metadata correct.
Common patterns
Pinning temporarily to a commit
go get github.com/org/lib@abcdef123456
This is a common short-term fix when waiting for a release.
Returning to a released version later
go get github.com/org/lib@v1.5.0
Once the release exists, teams often replace the pseudo-version with a proper tag.
Using replace for local development
If you are editing a dependency locally, use replace instead of a commit pin:
replace github.com/org/lib => ../lib
This is useful for local testing, not for publishing reusable module requirements.
Running go mod tidy
After dependency changes:
go mod tidy
This removes unused dependencies and keeps go.mod clean.
Validation in CI
Many teams commit both go.mod and , then CI runs:
Common Mistakes
1. Manually typing a pseudo-version incorrectly
Broken example:
require github.com/someone/some_module v0.0.0-20181121201909-deadbeef
If the timestamp or hash prefix does not match the real commit metadata, Go may reject it.
Better:
go get github.com/someone/some_module@af044c0995fe
Let Go generate the version.
2. Assuming @latest means latest commit
Broken assumption:
go get github.com/someone/some_module@latest
If the module has tagged releases, @latest usually means the latest appropriate release, not the newest commit on the default branch.
Use instead:
go get github.com/someone/some_module@main
or a commit hash.
3. Using replace when you really want a remote commit
replace github.com/someone/some_module => ../some_module
This works only in your local environment or repository setup. It does not mean “use the latest remote commit.”
Comparisons
| Approach | What it points to | Best for | Notes |
|---|---|---|---|
@v1.2.3 | Tagged release | Stable dependencies | Best default choice |
@latest | Latest selected version | Easy upgrades | Usually chooses a tag, not necessarily latest commit |
@main | Current branch tip | Trying unreleased changes | Can change over time if re-run |
@<commit> | Exact commit | Reproducible unreleased dependency | Go converts it to a pseudo-version |
replace => ../localpath | Local folder |
Cheat Sheet
# Use a specific commit
go get github.com/user/repo@abcdef123456
# Use a branch
go get github.com/user/repo@main
# Use a tagged version
go get github.com/user/repo@v1.2.3
# Clean up module files
go mod tidy
Key rules
- Prefer tagged versions for long-term stability.
- Use a commit hash when you need unreleased code.
- Let Go generate pseudo-versions automatically.
- Do not manually edit
go.sum. - Use
replacefor local development or local forks. @latestusually means latest release, not latest commit.
Pseudo-version shape
Examples:
v0.0.0-20181121201909-af044c0995fe
v1.2.3-0.20240102150405-abcdef123456
General idea:
- base semantic version
- commit timestamp in UTC
- commit hash prefix
Best practice
- Run
go get module@commit - Review
go.mod - Run
go mod tidy
FAQ
Can I put a Git commit hash directly in go.mod?
Not as the final stored version. You typically use the commit with go get module@commit, and Go converts it into a pseudo-version in go.mod.
Should I manually write pseudo-versions in go.mod?
Usually no. It is safer and more idiomatic to let the Go tool generate them.
What does the timestamp in a pseudo-version mean?
It is the commit time in UTC, formatted as yyyymmddhhmmss.
Does @latest fetch the newest commit from GitHub?
Not necessarily. It usually resolves to the latest appropriate released module version.
When should I use replace instead of a commit pin?
Use replace when testing a local copy of a module or temporarily pointing to a local fork.
Is a pseudo-version safe for production?
Yes, it is reproducible and valid, but teams often prefer switching to a tagged release when one becomes available.
Why does my module path need /v2 or higher?
In Go modules, major versions v2+ usually require the major version suffix in the module path.
Mini Project
Description
Create a small Go application that depends on a module using an unreleased commit. This project demonstrates how to pin a dependency to a specific commit, inspect the generated pseudo-version, and keep the module files clean.
Goal
Pin a Go dependency to a specific commit using go get, then verify the generated pseudo-version in go.mod.
Requirements
- Create a new Go module for a sample application.
- Add a dependency using a specific commit hash instead of a tagged release.
- Build the program successfully using that dependency.
- Run
go mod tidyand confirmgo.modandgo.sumare updated. - Inspect the resulting pseudo-version in
go.mod.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.