When should your team commit go.work, and when should the repo be a single module instead?
answer
- who outside the repo is affected
- relative paths only resolve in one layout
- a convenience, never a build requirement
- CI must see what a consumer sees
- one module until release lines diverge
basics
~20 sCommit 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.
solid answer
~50 sI decide it in two layers. Layout first: several modules in one repository buy independent version lines and let a consumer import a small piece without inheriting a heavy dependency set — and they cost separate releases per module, upgrade lockstep across them, and the loss of atomic cross-module changes. If nothing outside the team imports the pieces separately, one module is the cheaper default and the one I argue for. Then the file: `go.work` uses paths relative to itself, so it can only be committed when the entire workspace lives inside this repository; one spanning several checkouts is machine-specific by construction. Even committed, every module must still be built and tested with `GOWORK=off`, because that is the view consumers get. The release owner is right to overrule a committed `go.work` that hides a broken `go.mod` — the workspace is a convenience, never a build dependency.
code
text · 4 lines# CI: verify every module the way a consumer sees it
for m in api shared tools; do
(cd "$m" && GOWORK=off go build ./... && GOWORK=off go test ./...)
donego deeper
Know that go.work is normally a local file you do not commit, and that each module in the repository still has to build on its own.
Be able to explain why a workspace spanning several cloned repositories cannot be committed: its use entries are filesystem paths relative to the go.work file.
Argue the operational rule — no pipeline stage depends on a workspace, and every module is verified with GOWORK=off — and show how that makes committing the file a low-stakes choice.
Own both calls and their costs: how many modules the repository has, who outside depends on them separately, and the standing policy that reconciles developer convenience with the view consumers actually resolve.
## Two decisions that get confused with each other "Should we commit `go.work`?" is usually asked when the real question is "how many modules should this repository have?". Separate them, because the answers have different owners: repository layout is a design call the team lead owns, while whether a pipeline may rely on a workspace file is an operational call the release owner can and should overrule. ## Decision one: how many modules A module is a unit of *release and versioning*, not a unit of code organisation. Packages already organise code, and packages inside one module can import each other freely with no ceremony at all. Split into multiple modules when something is genuinely consumed independently: - A client library or plugin API that outside teams import, and that must move on its own version line rather than the service's. - A component with a heavy or awkward dependency set you do not want to force on every importer — dependency requirements travel with the module. - A part with a genuinely different release cadence or compatibility promise from the rest. Stay with one module when none of that is true. The costs of splitting are real and permanent: every module needs its own release step; a change touching two modules is no longer atomic, because the second module cannot require the first until the first is released; "what version is this repository" stops having one answer; and every developer now needs a workspace to do ordinary work. Teams routinely split for tidiness — one module per service directory — and then spend the next year paying release ceremony for a boundary nobody outside the repo can even observe. The question I ask to settle it: *who, outside this repository, needs to depend on one of these pieces at a version different from the others?* If the honest answer is nobody, it is one module. ## Decision two: commit the file or not `go.work`'s `use` entries are filesystem paths interpreted relative to the file. That single fact settles most cases: - A workspace spanning several separately cloned repositories **cannot** be committed usefully. The paths only resolve in one person's directory layout. It belongs in a personal ignore list, not in the repository. - A workspace whose `use` list is entirely inside this repository **can** be committed, and doing so is defensible when every developer genuinely wants the same set — a repo of modules that are always developed together. The benefit is real: a fresh clone is immediately in the right mode and nobody has to run `go work init` correctly. The question that decides it is not "does it work?" but "what does it hide?". A committed workspace means every build anyone runs from that checkout — including, if you are not careful, a build in the pipeline — resolves imports through the `use` list. A module whose `go.mod` is missing a requirement then compiles for everyone, forever, until the first outside consumer tries to import it. ## The rule that makes either choice safe **No pipeline stage may depend on a workspace.** Whether or not `go.work` is committed, CI builds, vets and tests each module from a clean checkout with `GOWORK=off`, which is precisely how a consumer resolves it. That job is cheap, needs no infrastructure, and converts the entire class of workspace-masked defects into a build failure on the pull request that introduced it. With that rule in place, committing `go.work` is a low-stakes convenience decision. Without it, committing `go.work` is a way to make a latent defect permanent, and a release owner who demands the file be deleted is not being obstructive — they are compensating for a missing gate. Give them the gate instead of the argument. ## Who is overruled, and on what grounds The team lead owns the layout and the developer experience. The person accountable for releases owns the requirement that every module builds the way consumers see it. When those collide, the release requirement wins, because it is the one with an external observer: a broken `go.mod` is visible to people outside the organisation and cannot be fixed by them. The converse also holds and is worth defending. A release owner who demands the deletion of every developer's local workspace file is overreaching — a local `go.work` in an ignore list harms nothing, and forbidding it just pushes people into hand-editing module files instead, which is worse and less visible. ## Migration, in both directions Going from several modules to one: confirm nothing outside imports the inner modules separately, merge the packages under one module path, delete the inner `go.mod` files, run `go mod tidy` on the single module, and delete `go.work`. The old import paths change, so anyone still depending on them needs notice and a plan. Going from one module to several: do it when the first genuine external consumer appears, not in anticipation of one. The split is easy to perform and hard to reverse socially, because once other teams pin the pieces separately you own those version lines forever. ## What I would write down One module by default. A second module only when an external consumer needs it on its own version line. `go.work` may be committed if the whole workspace is in this repository, is ignored otherwise, and in either case CI verifies every module with `GOWORK=off`. That is three sentences, and it prevents nearly every argument this topic generates.
- What is the strongest argument for splitting one repository into several modules?A part that outside consumers import on its own and that must move on its own version line — a client library, a plugin API, or a component whose dependency set you do not want to impose on every importer. Absent an external consumer, splitting mostly buys release ceremony and gives up atomic cross-module changes.
- Your release owner wants the committed go.work deleted. How do you respond?Ask what it broke. Their actual requirement is that each module builds as consumers see it, and a per-module `GOWORK=off` job gives them that without removing a developer convenience. If that job cannot be made green, they are right and the workspace is masking a defect. I would agree the standing rule that no pipeline stage may depend on the workspace.
- How do you migrate a multi-module repository back to a single module?First confirm nothing outside imports the inner modules separately. Then merge the packages under one module path, delete the inner `go.mod` files, run `go mod tidy` on the surviving module, and drop `go.work`. The inner import paths disappear, so anyone still depending on them needs advance notice and a migration window.
- Is it acceptable for a developer to keep an uncommitted go.work locally?Yes, and it is the normal case — especially for a workspace spanning several cloned repositories, which cannot be committed at all because its paths are machine-specific. Add it to a personal or repository ignore list. The only thing that must be enforced is that nothing shared depends on it existing.
saying these in an interview costs you the question
- Commits a go.work spanning repos on one machine
- Lets a pipeline stage rely on the committed workspace
- Splits into modules for tidiness with no external consumer
- Claims one module per directory is the Go convention
- Treats go.work as the project's dependency configuration