Why can a module build inside your go.work workspace but fail in CI, and how do you confirm it?
answer
- green on the laptop, red in CI
- the workspace is not in the checkout
- no requirement was ever needed locally
- ask which go.work is in effect
- turn the workspace off and rebuild
basics
~20 sWorkspace 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.
solid answer
~50 sIn workspace mode every directory in `go.work`'s `use` list is a main module, so an import of one from another resolves to local source no matter what the importing module's `go.mod` declares. A missing or stale `require` therefore never surfaces on the laptop. CI checks out the module without your `go.work`, resolves through that module's own requirements, and fails with `no required module provides package …`. I confirm it in two steps. First `go env GOWORK`, because the go command searches upward from the working directory and a workspace file two levels up still applies — that tells me a workspace is in play and which one. Then `GOWORK=off go build ./...` inside the module, which reproduces exactly the view CI and any consumer get. The fix is to make the module's own `go.mod` correct with `go mod tidy` under `GOWORK=off`, and to publish the sibling if it is genuinely a dependency.
code
text · 6 lines$ go env GOWORK
/home/dev/src/go.work
$ cd api
$ GOWORK=off go build ./...
handler.go:7:2: no required module provides package example.com/shared/auth; to add it:
go get example.com/shared/authgo deeper
Remember that a workspace only exists on the machine that has the go.work file. CI does not, so a module must be able to build on its own before you push.
Explain why the failure is invisible locally: workspace mode resolves the import from the use list before the module's own requirements are consulted, so a missing require costs nothing until the workspace is gone.
Walk the diagnosis in order — go env GOWORK to see which workspace applies, GOWORK=off to reproduce CI's view, then fix the module's go.mod — and say why making CI use the workspace is the wrong repair.
Set the rule that no pipeline stage may depend on a workspace, and make per-module verification with workspace mode disabled a standing gate rather than something each team remembers.
## The mechanism that hides the bug When a `go.work` file is in effect, every directory in its `use` list becomes a main module. Imports between those modules resolve to the local directories, and that resolution does **not** consult the importing module's `require` lines. The workspace answers the import before the module graph is ever asked. So this is possible, and it happens constantly on real teams: - `api/handler.go` gains `import "example.com/shared/auth"`. - `api/go.mod` has no `require example.com/shared`, or requires an old version that lacks the `auth` package. - The developer's `go.work` uses `./api` and `./shared`, so the import resolves to the sibling directory. Build green, tests green, review green. - CI clones the repo, builds that module, and there is no workspace — or the pipeline builds a container image from the module directory alone. Resolution goes through `api/go.mod`, finds nothing that provides the package, and stops. The error is explicit once you see it: ``` api/handler.go:7:2: no required module provides package example.com/shared/auth; to add it: go get example.com/shared/auth ``` The same shape of failure appears in a second, subtler form. A workspace builds all its modules against one selected set of dependency versions; a module built alone uses only its own declared requirements, which may be lower. Code that compiles locally against the higher version can fail to compile alone. Both cases have the same root: the workspace is a *view*, and the artefact you ship is the *module*. ## Confirming it, in order **1. `go env GOWORK`.** This prints the path of the `go.work` file the go command resolved, empty if none, or `off` if workspace mode was explicitly disabled. It matters because the go command searches the working directory and then every parent, stopping at the first `go.work` — so a file at the top of a monorepo checkout, or one left in a home directory, silently puts every build beneath it into workspace mode. Engineers who did not create the workspace are the ones this catches. **2. `GOWORK=off go build ./...` inside the failing module.** This is the whole diagnosis in one command: it turns off workspace mode without deleting anything and reproduces exactly what CI and any consumer see. If the local build now fails with the same message CI printed, the workspace was masking the defect and there is nothing else to investigate. Adding `GOWORK=off go test ./...` covers the same gap in test-only imports, which is where this hides most often. **3. Look at the module's `go.mod`.** With the workspace off, `go mod tidy` tells the truth: it resolves the module's imports through the module graph, not the `use` list, and will either add the correct `require` or fail because the sibling module has never been published — which is itself the answer. A module that imports another module that does not exist as a published version is not a module that can be built alone, and that has to be resolved deliberately rather than papered over. ## What not to do The tempting fix is to make CI look like the laptop: commit `go.work`, or generate one in the pipeline. That converts a caught defect into an uncaught one. The module's requirements stay wrong, and the failure moves to the first consumer who imports it — someone with no workspace at all, and no context. Even a repository that legitimately commits `go.work` because all its modules live together must still verify each module the way consumers resolve it. The rule that holds either way: **no pipeline stage may depend on a workspace.** ## Preventing the recurrence Give CI a job per module that runs from a clean checkout with `GOWORK=off` — build, vet and test. It is fast, it needs no extra tooling, and it is the exact view a consumer gets. Locally, running the same two commands before pushing catches the problem in seconds while the change is still in your head. It also helps to keep workspaces short-lived. A `go.work` created for one cross-module change and deleted when the library release lands cannot mask much. A `go.work` that has been sitting at the root of a checkout for a year, quietly satisfying imports for every build anyone runs, will eventually mask something — and the discovery will happen at the worst moment, in someone else's pipeline. ## The one-line summary for a review A workspace is a developer convenience layered over modules that must each stand alone. If deleting `go.work` breaks the build, the build was already broken.
- How do you stop this class of failure from reaching CI again?Add a job per module that builds, vets and tests from a clean checkout with `GOWORK=off`, which is precisely how a consumer resolves it. Running the same commands locally before pushing catches it in seconds. If the repository does commit a `go.work`, that job matters more, not less, because the workspace then exists in CI too.
- A colleague is in workspace mode without having created a workspace. How is that possible?The go command searches the working directory and every parent for `go.work` and uses the first one it finds, so a file at the top of a checkout — or higher — puts every build beneath it into workspace mode. `go env GOWORK` prints the resolved path; empty means ordinary module mode, and `off` means it was explicitly disabled.
- Would `go work sync` have prevented the missing requirement?No. `go work sync` only raises versions of requirements a module already declares to the versions the workspace selected. A dependency the module never required at all is not added, so the hole stays. Fixing that needs `go mod tidy` in the module with workspace mode off, and a published version of the sibling to require.
saying these in an interview costs you the question
- Blames a CI cache without checking GOWORK
- Commits go.work into CI to make the build pass
- Says go work sync would have added the missing requirement
- Claims go.work makes each module's go.mod redundant
- Assumes a module builds identically with and without a workspace