Workspaces are a feature introduced in March 2022 with Go 1.18. They did not really get a lot of publicity, and I have not had the chance to experiment with them until recently. However I am really glad I did because they improve a major aspect of my workflow: dealing with multiple modules.
Go modules are not ideal
Go modules were a big help when they were introduced in 2018, but they were always limited. Larger products are often split into multiple projects: one or more applications and several libraries and tools, meaning multiple modules.
While module versioning makes sure that components use the right version of
each dependency, it is also really annoying during development. If your
foo depends on the
go-bar library, you will often have to work
go-bar while testing the changes in
foo. This means updating
commiting changes, pushing them, and updating the
foo module with
A first improvement is the module replacement system. In the example above, we
could instruct Go to use the local copy of
go-bar when building
go-bar are at the same level in the filesystem:
go mod edit -replace example.com/myproject/go-bar=../go-bar
With this simple change, you have made your life easier: you can work on
go-bar, and immediately build and run
foo without any
additional operations. When you are done, you can still commit and push
But it is still not perfect. Module replacements are stored in the
file, meaning that these changes will be pushed to your central repository
circumventing dependency versioning and causing issues for other developers
and CI processes. While module replacement works fine to point a module to a
fork, it is not really a solution for our problem.
Go workspaces let you create environments where you control the source of the modules you use, without having to modify these modules.
Going back to our example, we can solve our problem by creating a workspace
foo in which
go-bar refers to the local copy. In the directory of
go work init go work use . go work use ../go-bar
Nothing complicated here. The
init subcommand creates the
which will contain the configuration of the workspace. Then the
subcommand is called to add two modules to the workspace: the
foo module in
the current repository, i.e.
., and the one in the
go-bar sibling directory.
At this point, building
foo will correctly use the local copy of
without having to modify any of the modules. Problem solved.
Even better, you can include
replace declarations in the
go.work file the
exact same way as in
go.mod file. These declarations will override those in
module files, giving you total control on the environment, again without
having to alter modules.
Committing the workspace file
Whether you commit the workspace file or not depends on your situation. When working in a mono-repository with other developers, committing a workspace file allows everyone to build the project the exact same way, using dependencies in the repository. It can also be practical with multiple repositories as long as you expect everyone to organize their local copies the same way.
But you can also keep your own workspace files without committing them. This
gives you the ability to quickly switch to a local copy for a dependency or to
replace a module by another one during development. For example this what I do
for my go-raft library project. The
program in the
cmd/kvstore directory is based on the go-service
library. During development, I
sometimes have to add code to go-service. Therefore I have a workspace file in
go-raft which references the local copy of go-service. But I do not commit it,
to avoid affecting anyone trying to build go-raft.
This flexibility is really practical, and I’m quite satisfied with workspaces at this point.
What could be better
There is a small issue to keep in mind. If you get used to work with workspaces, committing your local dependencies and having your program use them automatically, you may still have to maintain module dependencies. It is not necessarily a problem if you commit the workspace file and always use it, but it makes sense to keep dependencies up-to-date in your module files.
go work command has a
sync subcommand whose description let me think
that it would update modules included in the workspace to the right dependency
versions, but it turns out not do to so when tracking
pseudo-versions (i.e. when you are
tracking a branch and not a specific tag).
Therefore I have to continue to update non-tagged dependencies with this usual (and quite excessive) command, here for go-service:
GOPROXY=direct go get github.com/galdor/go-service@latest go mod tidy
This way I make sure to bypass the Go proxy,
fetch the last version of the
master branch and clean up the dependency
Despite this small inconvenience, Go workspaces have made my daily work much easier. Still a win!