Last Updated: May 22, 2026
A workspace is a thin layer that lets you develop multiple Go modules together as if they were one. You point a go.work file at a list of local module directories, and from then on the go toolchain treats edits in any of those modules as immediately visible to all the others, without publishing a new version and without editing go.mod files. Workspaces shipped in Go 1.18 and are the modern replacement for the old "add a replace directive, remember to remove it before committing" dance.
Consider a typical e-commerce backend split into four modules: a cart service, a catalog service, a pricing library, and an orders service. Each one lives in its own Git repository (or its own folder, if you use a monorepo), each one has its own go.mod, and they depend on each other through normal require lines:
Now you need to make a change in pricing, say a new function ApplyCoupon. You want to use that function from cart immediately to verify the change works end-to-end. The problem: cart's go.mod says pricing v1.4.0, and v1.4.0 doesn't have ApplyCoupon yet. Your new code only exists on your local disk.
Before Go 1.18, the workaround was a replace directive in cart/go.mod:
That works. The compiler now uses your local ../pricing instead of the version on the module proxy. But it has two annoying properties. First, you have to do this in every consumer module (cart, orders, catalog, all three need the same replace). Second, this is permanent until you remember to take it out. Forget once, and you commit a go.mod that says "use a path on Alice's laptop", which breaks the build for everyone else.
Workspaces fix both problems. The replace-style mapping moves out of go.mod and into a separate go.work file. The file is per-developer (or per-checkout) and isn't required to be committed. The go.mod files of the participating modules stay clean.
The go.work file at the top is what wires everything together. When the toolchain builds cart, it sees the go.work first, finds that pricing is listed there, and uses the local source on disk instead of resolving pricing v1.4.0 from the proxy. The go.mod files never change.
go work initThe starting point is go work init. Run it once, pass the directories of the modules you want to include, and it writes a go.work file.
Assume a directory layout like this:
From the shop/ directory:
That creates shop/go.work:
The go directive states the minimum Go toolchain version the workspace expects. The use block lists the local module directories that are part of the workspace. Each path is relative to the directory containing go.work (you can use absolute paths, but relative paths travel better between machines).
You can also run go work init with no arguments, which creates an empty workspace, and add modules later with go work use:
Both forms produce the same go.work. Pick whichever reads better in your shell history.
go work usego work use <dir> appends a directory to the use block of an existing go.work. This is the day-to-day command once a workspace is set up:
The go.work now looks like this:
If you have many modules under a single root, go work use -r. walks the directory tree recursively and adds every directory with a go.mod it finds:
For a monorepo with dozens of modules this is the natural way to bootstrap the workspace. You don't have to keep editing the use block by hand as new modules appear; rerun go work use -r. whenever the layout changes.
Removing a module is the reverse: go work use with a path that's already in the use block does nothing. To drop a module, you either edit go.work by hand or use go work edit -dropuse <dir>:
go work edit is the scriptable interface to the file. It has flags for adding (-use), removing (-dropuse), and adjusting replace directives (-replace, -dropreplace). For interactive use, editing go.work directly is usually fine; for CI scripts or automation, go work edit is the safer choice because it never produces malformed output.
go.work File StructureA go.work file is small. It has at most three kinds of directives:
| Directive | Purpose |
|---|---|
go | Minimum Go toolchain version for the workspace |
use | A local module directory to include in the workspace |
replace | A workspace-level replacement, same syntax as in go.mod |
Here's a more complete example for the e-commerce setup:
The go directive works the same way it does in go.mod. The use block is the workspace's primary content. The replace block, if present, applies to every module in the workspace at once. That's a meaningful difference from go.mod-level replace: a workspace replace doesn't have to be repeated in cart/go.mod, orders/go.mod, and catalog/go.mod separately.
When you build anything inside the workspace, the toolchain computes the effective module graph by:
go.mod listed in use.import "github.com/shop/pricing" resolves to ./pricing, not the registered version).replace directives on top.require lines via the proxy as usual.So in practice, the modules listed in use see each other locally, and everything else (third-party dependencies, modules not in the workspace) flows through the normal module resolution.
go work use -r. walks the directory tree to find modules. On a very large monorepo (hundreds of modules and deep directory trees), this can take a few seconds. It's a one-time cost, not a per-build cost.
go.work.sum and Why It ExistsAlongside go.work, the toolchain may create a go.work.sum file. This is the workspace's analog of go.sum, but only for dependencies that aren't already accounted for by the participating modules' own go.sum files.
Here's why it exists. When the workspace pulls modules together, the effective build list can include versions of third-party dependencies that no single go.mod in the workspace pinned on its own. Maybe cart/go.mod requires github.com/google/uuid v1.3.0 and orders/go.mod requires v1.3.1. The workspace's combined module graph picks the higher version (Go's minimum version selection), and that higher version's checksum needs to live somewhere. The individual go.sum files might not have it. go.work.sum does.
You don't write go.work.sum by hand. The toolchain creates and updates it during normal commands (go build, go test, go mod tidy within the workspace). Like go.sum, you can commit it if you want reproducible workspace builds across a team, or leave it out if every developer maintains their own workspace.
This is the part that surprises people the first time. There's no flag to "enter workspace mode". The toolchain enters it automatically based on what it finds on disk, and you can override it with an environment variable.
The discovery rule is:
go.work file is found anywhere along that path, workspace mode is on, and that go.work is the active workspace.go.work is found by the time the walk reaches the filesystem root, the toolchain falls back to single-module mode (find the nearest go.mod the same way and use it alone).So if you cd shop/cart/internal/handlers and run go build, the toolchain walks up: handlers/, internal/, cart/, shop/. It finds shop/go.work and enters workspace mode. Imports of github.com/shop/pricing resolve to ./pricing (local), not the module proxy.
If you instead cd /tmp/scratch and run a build there, no go.work exists upward, so it's single-module mode (or no module at all, depending on whether there's a go.mod).
Two environment variables let you override discovery:
| Variable | Effect |
|---|---|
GOWORK=off | Disable workspace mode. Toolchain ignores any go.work it finds and behaves as if the workspace doesn't exist. |
GOWORK=/path/to/go.work | Use the specified file as the active workspace, regardless of discovery. |
GOWORK=auto (default) | Use the discovery rule above. |
GOWORK=off is handy when you want to test that your go.mod files are correct on their own, without the safety net of the workspace. Run your test suite with GOWORK=off go test./..., and is the same thing CI will see when it builds your module without your local workspace.
The diagram shows the decision tree the toolchain runs on every command. The default GOWORK=auto falls into the discovery walk. GOWORK is rarely set explicitly, so workspace mode kicks in transparently whenever a go.work is present, and stays out of the way when it isn't.
Concretely, here's how the workflow plays out for the cart and pricing example. Suppose pricing/pricing.go currently exports one function:
And cart/main.go uses it:
cart/go.mod requires the published version:
You decide to add a new function ApplyCoupon(amount float64, code string) to pricing. You edit pricing/pricing.go on disk:
You want to use ApplyCoupon from cart/main.go right away to see it work end-to-end. Without a workspace, you'd add a replace directive in cart/go.mod and remember to remove it. With a workspace, you do this once:
Then update cart/main.go to call the new function:
From inside cart/:
Note that cart/go.mod still says require github.com/shop/pricing v1.4.0. The toolchain didn't pull v1.4.0 from the proxy; it used the local ./pricing source. When you're done and ready to ship, you publish a new pricing version (say v1.5.0), update cart/go.mod to require it, and either delete go.work or remove ./pricing from the use block. The committed cart/go.mod never had a temporary replace line.
go work syncgo work sync is a smaller but useful command. When the workspace selects a higher version of a shared dependency than what any individual module pinned, go work sync propagates that selection back down into each module's go.mod.
Here's the scenario. Both cart and orders use github.com/google/uuid:
The workspace's combined module graph picks v1.4.0 (the higher version). When you build inside the workspace, that's what gets used. But if you build cart alone outside the workspace (or in CI), it sees v1.3.0. The local workspace and the external build disagree.
go work sync fixes this by bumping cart/go.mod to also require v1.4.0:
Now cart outside the workspace builds against the same uuid version the workspace was using. go work sync is typically a step you run before committing a coordinated change across multiple modules. It's not something you run continuously.
go work sync rewrites the go.mod files of every module in the workspace. If your CI pipeline checks for clean working directories, run sync early in your change so the resulting go.mod updates land in the same commit.
replace DirectivesThe replace directive in go.mod is the older mechanism for redirecting a module to a local path. Workspaces are the newer one. They overlap in capability, but they're for different jobs.
Workspace (go.work) | go.mod replace | |
|---|---|---|
| Scope | All modules listed in use see the local source | One module at a time; must be repeated everywhere |
| Persistence | Per-developer, typically not committed for libraries | Committed alongside source code |
| Easy to forget | No, since go.mod stays clean | Yes, accidental commits of replace../../local are common |
| Use case | Active multi-module development | Permanent forks, mirrors, monorepo-internal pinning in production |
| Removal cost | Delete go.work or go work edit -dropuse | Edit every go.mod that has the replace |
| Multi-machine portability | Bad (paths are local) | Same, if absolute paths are used |
| Affects external consumers | No (they never see go.work) | Yes, an unintended replace ships to consumers |
The short version: use a workspace when you're editing two or more modules together during development. Use a replace directive when you genuinely want to redirect a module permanently in production code, for example to point at an internal fork of a third-party library. Unlike production replace directives covered in the Module Versioning chapter, a workspace's replace (and use) lives entirely outside the committed go.mod files.
A common pattern in mature codebases is: workspaces during local development, no replace in go.mod, and a clear vendor/ directory (or strict module proxy) for production builds. Each tool does one job.
go.work?This is the question that comes up on every team that adopts workspaces. The answer depends on the kind of project.
Commit `go.work` when:
Don't commit `go.work` when:
For most open-source Go libraries on GitHub, the convention is to add go.work and go.work.sum to .gitignore. Each contributor creates their own when they need one. For a private monorepo where every developer checks out the entire repository and builds everything, committing go.work saves the "first time setup" step and gives everyone the same view.
A reasonable middle ground for repositories that aren't sure: provide a Makefile target or a setup script that creates the workspace, but don't commit the file itself. That way new contributors run make workspace (or similar) once, and the resulting go.work is local to their checkout.
Workspaces are powerful for local multi-module development, but they're not a replacement for the broader module system. A few cases where they're not the standard tool:
GOWORK=off to make this explicit, or rely on the fact that CI checkouts won't have a go.work if you don't commit it.go.mod, not your go.work. Anything that needs to be visible to consumers (a real replace for a permanent fork, the actual require versions) belongs in go.mod.The mental model that works best: a workspace is a developer-side overlay on top of the regular module system. It changes what the toolchain sees on your machine. It doesn't change what's published, what's pinned, or what anyone else sees.