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:
v0.0.0: a base version used when no suitable earlier tag applies20181121201909: UTC timestamp of the commit inyyyymmddhhmmssformaf044c0995fe: short commit hash
Example workflow
go get github.com/someone/some_module@af044c0995fe
go mod tidy
Then inspect go.mod:
require github.com/someone/some_module v0.0.0-20181121201909-af044c0995fe
This is preferred over manually inventing the pseudo-version string.
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:
module myapp
go 1.22
require github.com/example/lib v1.4.1
You run:
go get github.com/example/lib@a1b2c3d4e5f6
Now it may become:
module myapp
go 1.22
require github.com/example/lib v1.4.2-0.20240201112233-a1b2c3d4e5f6
That tells Go: use the specific commit, even though there is no final v1.4.2 release yet.
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 go.sum, then CI runs:
go mod tidy
ngo build ./...
This helps catch accidental or inconsistent dependency changes early.
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.”
Use replace only for local overrides or forks.
4. Forgetting major version module paths
For modules using v2 or higher, the path often includes the major version suffix:
github.com/example/lib/v2
If you omit /v2, version resolution can fail.
5. Keeping a commit pin forever
A pseudo-version is often useful temporarily, but long-term it is usually clearer to move back to a tagged release once available.
6. Editing go.sum manually
Do not do this. Let Go maintain it automatically.
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 | Local development | Not a remote version pin |
Pseudo-version vs tagged version
| Version type | Example | Human readability | Stability |
|---|---|---|---|
| Tagged version | v1.4.0 | High | High |
| Pseudo-version | v1.4.1-0.20240201112233-abcdef123456 | Medium | Good, but usually temporary |
Manual edit vs go get
| Method | Recommended? | Why |
|---|---|---|
| Edit pseudo-version by hand | Usually no | Easy to make mistakes |
go get module@commit | Yes | Go computes the correct version for you |
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 - Commit both
go.modandgo.sum - Switch back to a tag when a release becomes available
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
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.