What does a `replace` directive in go.mod do, and how do you point one at a local directory?
answer
- a redirect written in your own go.mod
- two right-hand forms, only one has a version
- a directory target needs its own go.mod
- it swaps the source, it adds no requirement
basics
~20 sA replace directive in go.mod redirects a module path to something else: either a different module at a stated version, or a directory on disk. The build then compiles that code instead of the version the proxy would serve.
solid answer
~50 s`replace` rewrites where the go command gets a module's source. The left-hand side is the module path being redirected, optionally with a version to restrict the redirect to that one version; leaving the version off redirects every version. The right-hand side takes one of two forms. A module path **with** a version (`replace example.com/lib => example.com/ourfork/lib v1.4.2`) is fetched and checksum-verified like any other module, so it needs `go.sum` entries. A **filesystem path** (`replace example.com/lib => ../lib`) carries no version, is compiled straight from disk, and the target directory must contain its own `go.mod`. Two things surprise people: a `replace` does not add a requirement, so if nothing in the build list needs that module the line is simply inert; and only the main module's `replace` lines are honoured, so a library cannot redirect its importers.
code
mod · 14 linesmodule example.com/internal/billing
go 1.25
require (
example.com/upstream/parser v1.4.2
example.com/shared/auth v0.6.0
)
// Module form: a version is required, and go.sum gains entries for the fork.
replace example.com/upstream/parser => example.com/ourorg/parser-fork v1.4.2
// Directory form: no version, and ../auth must contain its own go.mod.
replace example.com/shared/auth => ../authgo deeper
Be ready to write both forms from memory and say what each right-hand side needs: a module path takes a version, a directory takes none and must hold its own go.mod.
Explain that a replace rewrites the build list rather than adding to it, and describe what changes in go.sum for a module-path target versus a directory target.
Show how you would verify a redirect actually fired — go list -m all printing the original => replacement pair — and why a directory replacement is fine in a branch but risky to merge.
Own the policy question: which replaces are allowed into a repository at all, whether a filesystem path may ever reach the default branch, and how the team is stopped from shipping one by accident.
## What the directive is for A `go.mod` file records the module's own path, a language version, and a set of `require` lines naming the modules it depends on and the minimum version of each. Normally the go command resolves those requirements, downloads the exact versions from a module proxy, and compiles them. A `replace` directive interrupts that: it tells the go command that wherever the build would have used a particular module, it should use something else instead. The two things it is used for in practice are (1) developing two modules side by side, where you want your application to compile against the checkout of a library sitting next to it rather than a published release, and (2) shipping against a fork that carries a patch you need before upstream has released it. ## The syntax, both halves ``` replace old-module-path [old-version] => new-module-path new-version replace old-module-path [old-version] => ../some/directory ``` **Left-hand side.** The module path being redirected. The version is optional. `replace example.com/lib => …` redirects *every* version of `example.com/lib` that the graph resolves to. `replace example.com/lib v1.4.2 => …` redirects *only* v1.4.2 and leaves other versions alone — useful when you want the redirect to lapse automatically the moment resolution moves to a different version. **Right-hand side, module form.** `=> example.com/ourfork/lib v1.4.2`. A version is mandatory here. The replacement is downloaded and hashed exactly like any other dependency, so `go.sum` gains entries for the *replacement*, not for the module it stands in for. The replacement's own `go.mod` requirements are what feed the module graph from that point on. **Right-hand side, directory form.** `=> ../lib` or an absolute path. A version must **not** be given — the code on disk has no version. The directory must contain a `go.mod` file; a bare package directory is rejected. Nothing is downloaded and nothing is checksum-verified, which is exactly why this form is convenient during development and unwise to ship: the build now depends on a path outside the repository. ## Three rules that catch people out **A replace does not add a requirement.** If no `require` line and nothing in the transitive graph pulls in `example.com/lib`, then `replace example.com/lib => …` does nothing at all. It is a rewrite rule applied to the build list, not a way to introduce a dependency. There is no error and no warning for a replace that never fires, and `go mod tidy` will not remove it. **Only the main module's replaces apply.** The main module is the one containing the directory you invoke the go command from. Replace lines in the `go.mod` of any dependency are read and ignored. That makes the directive safe — a transitive dependency cannot silently swap out the source of code you compile — but it also means a published library whose build only works because of its own replace line is broken for everyone who imports it. **Versions on the right-hand side are used verbatim.** Minimal version selection does not run on a replacement target. Whatever version you write is the version compiled, even if a newer release of the fork exists. ## Editing without hand-writing the file `go mod edit` manipulates the directives programmatically, which is handy in scripts and CI: ``` go mod edit -replace=example.com/lib=example.com/ourfork/[email protected] go mod edit -replace=example.com/lib=../lib go mod edit -dropreplace=example.com/lib go mod edit -json # prints the whole file, directives included, as JSON ``` `go mod edit -json` is the quickest way to see the replace block a build is actually resolving with, which matters once a repository has accumulated several of them. ## Checking it took effect After adding a replace, `go list -m all` prints the build list with each replacement shown as `original => replacement`. If the line you added does not appear there, the module was not in the build list to begin with, and the redirect never fired.
- Does a `replace` line make the go command fetch a module that nothing in your build requires?No. A replace is a rewrite applied to modules already in the build list. If nothing requires `example.com/lib`, the line simply never fires — no download, no error, no warning, and `go mod tidy` leaves the dead line in place. To bring a module in you still need a `require`.
- How does `go.sum` differ between the two right-hand-side forms?A module-path replacement is downloaded and verified like any dependency, so `go.sum` needs hashes for the replacement module and version — not for the module it stands in for. A directory replacement is compiled from disk, so there is nothing to hash and no `go.sum` entry at all.
- Can you redirect only one version of a dependency and leave the rest alone?Yes — put a version on the left: `replace example.com/lib v1.4.2 => example.com/ourfork/lib v1.4.2`. Only v1.4.2 is redirected. If resolution later moves the build to v1.4.3, the replacement stops applying, which makes the redirect self-expiring.
saying these in an interview costs you the question
- Says a replace propagates to everyone importing the module
- Puts a version on the right side of a directory replacement
- Thinks the replaced directory does not need its own go.mod
- Believes a replace alone pulls in an unrequired module
- Claims minimal version selection runs on the replacement target