Nicolas Martyanoff – Brain dump About

Golang workspaces, at last!

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 application foo depends on the go-bar library, you will often have to work on go-bar while testing the changes in foo. This means updating go-bar, commiting changes, pushing them, and updating the foo module with go get. Quite cumbersome.

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 foo. Assuming that foo and 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 changes in go-bar, and immediately build and run foo without any additional operations. When you are done, you can still commit and push normally in go-bar.

But it is still not perfect. Module replacements are stored in the go.mod 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.

Using workspaces

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 for foo in which go-bar refers to the local copy. In the directory of foo:

go work init
go work use .
go work use ../go-bar

Nothing complicated here. The init subcommand creates the go.work file which will contain the configuration of the workspace. Then the use 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 go-bar 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.

The 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 list.

Despite this small inconvenience, Go workspaces have made my daily work much easier. Still a win!

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.