April 5, 2022·5 min read·engineeringgoworkspacesmodules

Go workspaces separate local development from module requirements

The Go team's workspace guidance explains how to develop related modules without temporary edits to every go.mod. Workspace selection and dependency synchronization remain distinct operations.

A local dependency override is useful during development and easy to misunderstand after it travels with a module declaration. One engineer needs a checkout on disk, while another consumer needs a dependency it can download. Go's workspace mode gives the local arrangement its own file, so an edit spanning modules does not require recording every temporary path in their go.mod files.

Beth Brown's 5 April workspace guide explains the workflows introduced with Go 1.18. The feature itself arrived in the 15 March release. April's contribution is a more concrete account of development across independently declared modules, including changes to an upstream dependency and work within a repository containing several modules.

I like this separation because Go is easiest to maintain when a file's purpose remains clear. A module declaration should explain the module to its consumers. A workspace can explain how a developer is assembling local source without asking every consumer to reproduce that directory layout.

A directory becomes a main module explicitly

The command documentation shipped in Go 1.18 defines a workspace through go.work. A use directive selects a directory containing go.mod. Selected modules form the set of main modules used in workspace builds and related operations. Selection is an explicit declaration about local source.

Consider a parent directory holding an application checkout and a library checkout. Both already have module declarations. From the parent directory, this command creates the workspace configuration.

go work init ./application ./library

The file records the two local directories. It does not assign new module paths to them or combine their declarations into one module. A library import still uses the module's declared path. The workspace changes which local source participates in resolving that import.

That matters when a repository contains a nested module. Directory containment and module membership answer different questions. Listing the outer module does not implicitly declare every nested module part of the workspace. The developer must select the relevant module directories, or use the recursive form of go work use to discover them.

The workspace documentation describes go work use -r for this recursive selection. It is convenient for a collection of modules whose membership follows the directory tree. Explicit paths may be easier to understand when only a subset should participate. Either form produces a concrete set of main modules that can be inspected.

The local build and the consumer build differ

The April guide describes developing a change in an upstream module while using it in another module. Workspace mode allows that experiment before the upstream change has been published. This removes the need to repeatedly insert and remove a temporary local replacement in the dependent module's go.mod.

The convenience does not make the unpublished dependency available to other consumers. A workspace build proves something about the checked-out source selected by its configuration. Publishing the upstream change remains necessary when a downstream release is supposed to consume it independently.

This distinction is easy to miss because the application can appear finished inside the workspace. Its imports compile against the library under development, and the local tests exercise that arrangement. A consumer with access only to the application's declared dependencies may see a different library revision. The difference follows from source selection, not from a failure of the compiler to check the local program.

My preference is to describe the intended consumption mode alongside the development workflow. A repository that deliberately builds a coordinated set of modules has different evidence from a library distributed on its own. Both are legitimate, but a successful combined build should not be described as proof of independent consumption.

Configuration needs to be observable

The release notes explain workspace discovery through the current directory, parent directories and the GOWORK environment variable. That means the selected workspace is part of the command's environment. A parent configuration can affect a command issued from a module directory.

The versioned command documentation gives a direct way to inspect that state. Running the following command reports the workspace file in use.

go env GOWORK

That is a useful first fact when comparing results between an editor and a terminal, or between two directories. The explanation may be a different set of main modules. There is little value in discussing a dependency-resolution discrepancy until the participating local modules are known.

Replacements have another explicit precedence rule. A replace directive in go.work takes precedence over replacements in individual module files. The workspace documentation identifies conflicting replacements as an important use case. A combined development arrangement needs somewhere to state which replacement wins.

Replacement editing uses go work edit. For example, with an existing workspace, this command records a local replacement without editing a module's requirements.

go work edit -replace=example.com/library=../library

The path on the right must be the intended local module directory. This command's spelling comes from the released command documentation. The April article's GOPATH section mentions go work replace, but the documented subcommand for editing a replacement is go work edit with its replacement flag.

Synchronization can change the modules

Creating a workspace and synchronizing its dependency choices are separate actions. Go work sync computes the combined build list using minimal version selection and writes the selected dependency versions back into the participating modules. The documentation specifies upgrades where a module's existing dependency version is lower than the workspace choice.

That makes sync materially different from choosing local source directories. A script that creates go.work establishes a development arrangement. A script that also invokes sync can change the requirements distributed with each module. Those changes deserve review as dependency changes, even when the commands run during local setup.

Keeping the operations distinct helps explain the resulting diff. A developer may want to test coordinated source changes without immediately propagating every combined dependency choice into each module. Synchronization is available when those choices should become module requirements.

I regard the workspace file as useful because it gives that coordination an inspectable representation. Its value depends on preserving the separate meaning of each module's own declaration. Local source selection answers which code a development command sees, while synchronized requirements answer which dependency versions a module records for subsequent consumers.