Question

How to Use a Specific Git Commit in go.mod for a Go Module Dependency

gogitmodule

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.0 represent a base version,
  • does 20181121201909 represent the commit timestamp,
  • and does af044c0995fe represent 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.0 is 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.

Take Quiz

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 applies
  • 20181121201909: UTC timestamp of the commit in yyyymmddhhmmss form
  • af044c0995fe: 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

  1. The Go tool looks up the module path:

    github.com/example/lib
    
  2. It resolves the revision:

    a1b2c3d4e5f6
    
  3. It checks the repository metadata, including:

    • the exact commit time
    • whether there are relevant tags before that commit
    • the correct major version path rules
  4. It generates a valid pseudo-version, for example:

    v1.4.2-0.20240201112233-a1b2c3d4e5f6
    
  5. It writes that version into go.mod.

  6. It records cryptographic checksums in go.sum.

  7. 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.

Take Quiz

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

ApproachWhat it points toBest forNotes
@v1.2.3Tagged releaseStable dependenciesBest default choice
@latestLatest selected versionEasy upgradesUsually chooses a tag, not necessarily latest commit
@mainCurrent branch tipTrying unreleased changesCan change over time if re-run
@<commit>Exact commitReproducible unreleased dependencyGo converts it to a pseudo-version
replace => ../localpathLocal folderLocal developmentNot a remote version pin

Pseudo-version vs tagged version

Version typeExampleHuman readabilityStability
Tagged versionv1.4.0HighHigh
Pseudo-versionv1.4.1-0.20240201112233-abcdef123456MediumGood, but usually temporary

Manual edit vs go get

MethodRecommended?Why
Edit pseudo-version by handUsually noEasy to make mistakes
go get module@commitYesGo computes the correct version for you
Take Quiz

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 replace for local development or local forks.
  • @latest usually 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

  1. Run go get module@commit
  2. Review go.mod
  3. Run go mod tidy
  4. Commit both go.mod and go.sum
  5. 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.

Take Quiz

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 tidy and confirm go.mod and go.sum are updated.
  • Inspect the resulting pseudo-version in go.mod.
Take Quiz

Keep learning

Related questions

View more