skip to content

Workspaces (go.work)

Go 1.18's answer to editing two modules at once: a local go.work file that overrides the module graph, so you stop committing temporary replace lines and forgetting to remove them. Comes up whenever a team splits a repo into shared libraries.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What is a Go workspace, and what does the go.work file created by `go work init` do?

level: juniorimportance: should knowfreq 45%

answer

  1. several checkouts, one build
  2. a file that sits above the modules
  3. it lists directories, not versions
  4. local source wins over the proxy
  5. no module's go.mod is edited

basics

~20 s

A Go workspace makes the go command treat several local module directories as the modules being built. go work init writes a go.work file listing those directories under use, so imports between them resolve to your local code instead of published versions.

solid answer

~50 s

A workspace is a `go.work` file that lists module directories in a `use` block. When the go command finds a `go.work` — it searches the working directory and each parent — it runs in workspace mode: every listed module is treated as a main module, and an import of one listed module from another resolves to that local directory instead of to a version downloaded from the module proxy. `go work init ./api ./shared` creates the file with a `go` line and those two `use` lines; `go work use ./tools` adds another later. It changes nothing inside any module's `go.mod`: the modules stay independent, each with its own module path and requirements. That is what makes it the normal way to develop a service and a library it imports side by side, without editing either module's declared requirements while you work.

code

mod · 7 lines
mod
go 1.27

use (
	./api
	./shared
	./tools
)

go deeper

for a junior

Be ready to name the problem it solves: editing a service and a library it imports at the same time, before the library is released. Know that go work init creates go.work and that each use line points at a directory containing a go.mod.

for a middle

Explain the mechanics: the go command searches upward for go.work, every use'd module becomes a main module, imports between them resolve to local source, and no module's go.mod is modified.

for a senior

Show that you treat a workspace as a local convenience. Anything CI or a consumer builds must resolve without it, and you can prove that with GOWORK=off before you push.

for a principal

Own the policy question underneath: whether the repo holds several modules at all, whether go.work is committed, and who is accountable for each module still building the way its consumers see it.

## The problem a workspace solves A Go module is the unit of versioning: a directory with a `go.mod` that declares a module path and the versions of other modules it requires. Normally the go command resolves an import to either a package inside the current main module or a package inside a specific module version it has downloaded into the module cache. That works well until you are changing two modules at the same time — a service and the shared library it imports, say. The library change is not published yet, so the service cannot see it; publishing a version just to test a two-line change is absurd. A workspace makes the go command build several local modules together. Instead of one main module, it takes a set of them. ## The file `go work init ./api ./shared` writes a `go.work` in the current directory: ``` go 1.27 use ( ./api ./shared ) ``` The directives are few. `go` gives the language version for the workspace. `use` names a directory that contains a `go.mod`; the paths are filesystem paths interpreted relative to the `go.work` file, not module paths. A `go.work` may also carry `replace` and `toolchain` directives, but the `use` list is the heart of it. You rarely hand-edit it. `go work init [dirs...]` creates it, `go work use ./tools` appends a directory, `go work use -r .` walks a tree and adds every module it finds, and `go work edit -dropuse ./tools` removes one. ## Workspace mode When a `go.work` is in effect, the go command is in *workspace mode* and every `use`d module is a main module. Three consequences matter: 1. **Imports between workspace modules resolve locally.** If `./api` imports a package from the module rooted at `./shared`, the go command compiles the source in `./shared` — whatever is in your editor right now — rather than a downloaded version. 2. **`./...` at the workspace root spans all of them.** `go build ./...` from the directory holding `go.work` builds every `use`d module's packages. 3. **No `go.mod` is touched.** `go work init` and `go work use` only write `go.work`. Each module keeps its own module path, its own `require` lines, and is still built, tagged and consumed independently of your workspace. That third point is the one people miss, and it cuts both ways. It is the whole benefit — you get to try a cross-module change without dirtying files you would have to remember to revert — and it is the whole risk, because a module can compile inside the workspace while its own `go.mod` is wrong. ## How the file is found, and how to switch it off The go command searches the current working directory and then each parent directory for `go.work`, stopping at the first one. So a `go.work` at the top of a checkout applies to builds run from anywhere below it, including inside a single module — you do not have to be standing in the workspace root for it to take effect. This is also how a workspace surprises people who did not create it. The `GOWORK` environment variable overrides the search. `GOWORK=/path/to/go.work` names an explicit file, and `GOWORK=off` disables workspace mode entirely, putting the go command back into ordinary single-module mode. `go env GOWORK` prints which file, if any, is currently in effect; that is the first thing to check when a build behaves differently from what you expect. ## What a workspace is not It is not a way to merge modules into one. Each module retains its identity; a workspace is a view layered over them for the duration of your work. It is not a build configuration your project depends on. Anything shipped — a tagged release, a consumer's `go get`, a CI pipeline — resolves through the module's own `go.mod`, never through your `go.work`. A workspace that is required for the build to succeed is a symptom, not a design. It is not needed for a single module with many packages. Packages inside one module already see each other; a workspace is for crossing module boundaries. And it does not have to be one repository. Because `use` paths are filesystem paths, a workspace can span several cloned repositories on one machine — which is exactly why such a `go.work` cannot be usefully committed anywhere: the relative paths only make sense in that person's directory layout. ## The everyday loop Clone the two or three repositories (or open the one repo that holds several modules), run `go work init` naming their directories, and work. Build, test and run as usual. When you are done, the library change gets released on its own, the service's `go.mod` is updated to require that release, and the workspace becomes irrelevant again — you can delete `go.work` or leave it for the next round of work.

  • Where does the go command look for go.work, and how do you turn workspace mode off?
    It searches the working directory and each parent for `go.work` and uses the first one it finds, so a workspace two levels up still applies when you build inside a module. The `GOWORK` environment variable overrides that: set it to a specific file path, or `GOWORK=off` to ignore any workspace and resolve through the module's own `go.mod`.
  • Does adding a directory to go.work change that module's go.mod?
    No. `go work init` and `go work use` write only `go.work`. Each module keeps its own module path, `require` lines and `go.sum`, and is still built and released on its own. The workspace is a view layered on top, which is why every module must remain buildable after you delete the file.
  • Do the modules in a workspace have to live in the same repository?
    No. The `use` entries are filesystem paths relative to `go.work`, so a workspace can span several repositories cloned side by side on one machine. That also makes such a file machine-specific: the relative paths only resolve in that particular directory layout, so it cannot be committed usefully.

saying these in an interview costs you the question

  • Says go.work must be committed or the build breaks
  • Thinks go work init rewrites each module's go.mod
  • Confuses a workspace with one module holding many packages
  • Believes a workspace merges the modules into a single module
  • Assumes go.work is needed for any multi-package project
open as a page

Why can a module build inside your go.work workspace but fail in CI, and how do you confirm it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Workspace mode satisfies an import from a sibling directory in go.work's use list even when that module's own go.mod never requires it, so a missing requirement is invisible locally. CI builds the module alone and fails. Confirm by rebuilding with GOWORK=off.

open as a page

When should your team commit go.work, and when should the repo be a single module instead?

level: principalimportance: should knowfreq 32%

basics

~20 s

Commit go.work only when every directory it uses lives in this repository and every developer wants that same set, and only as a convenience no pipeline depends on. Default to one module unless parts genuinely need independent release lines.

open as a page

In a Go workspace, what does `go work sync` change in each module's go.mod?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

go work sync computes one dependency version list for the whole workspace, then writes those selected versions back into the go.mod of each module the workspace uses. It raises requirements a module already declares; it does not add new ones.

open as a page