I’ve been relatively conservative when it comes to Go modules, as they came on to the scene there was the normal process where the change wasn’t 100% meeting expectations. One of those changes was being able to work on local code and use it, without the build process downloading the published version that did not (yet) have the changes.
The initial solution was to use the replace
directive in a go.mod
, but this meant that developers could (and would) accidentally push go.mod
files to their repo, which would break production builds because the filesystem on prod needed to match what the developer locally in order for the replace
to work.
Example of replace
being used in go.mod
module github.com/shanehowearth/cloudblog/REST
go 1.18
require (
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/render v1.0.1
github.com/shanehowearth/cloudblog/readarticle v1.25.1
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
)
replace github.com/shanehowearth/cloudblog/readarticle => ../readarticle
Another solution was to vendor
versions of the local code so that the production build system would build with the vendored versions, instead of published versions.
Obviously these are hacks, and are brittle.
I’ve recently been working with a multi-repo/module system, a set of microservices that combine to create a backend system. One of the microservices provides a set of common functions for access to one of the datastore types in use. I created a new function for access, because changing the old function would have broken backward compatibility for a number of downstream, dependent microservices.
In order to make use of that new function in one (or more) of the downstream services, I would normally have to push/publish the (untested) code to our repo (so that the build/test applications locally would fetch the updated service), or use areplace
directive in my go.mod
.
Enter Go workspaces. Workspaces provide an easy way for developers to use local changes to other repos in tests. Incredibly easy.My first read of the tutorial (that I linked to earlier) left me confused. So that’s why I am writing this, because workspaces make life super easy.
The steps are as follows- we’re going to assume that, like I was, you are dealing with established codebase(s) that make use of go modules
.
- Initialise the workspace (this creates a
go.work
file, and ago.sum
file)
$ cd $REPO
$ go work init
- Edit the
go.work
file to tell go what local repos to use examplego.work
go 1.18
use ./
use ../readarticle
That’s it, that’s everything you need to do.
For an explanation of the lines
go 1.18
is the minimum Go version to use
use ./
means to use the code (relative to this go.work
) in this directory
use ../readarticle
means to use the code found in ../readarticle
(again the path is relative to this go.work
file location)
The build/test tools know from the path, and module name, what module is being replaced, and does so.
There’s no messy version numbers (that I have seen used in replace
directives) there’s no chance of accidentally pushing a bad go.mod
, and, because of the way I have configured my git
there’s no pushing of go.work
to prod.
Be aware, though, that if you do push that file (or want to build a container locally) then you will have problems similar to pushing a bad go.mod
.
The solution is to not track the file(s), put go.work
into your global gitignore
, mine is at ~/.gitignore
, and I have configured git to use that with
git config --global core.excludesFile ~/.gitignore
You could also put rules into your Dockerfile
, or docker-compose.yml
or build scripts, but I leave that as an exercise for the more knowledgeable :-)
Summary
Using Go workspaces is a dream compared to all the hangnails I used to deal with with replace
directives (or vendoring), so get on the workspace train, and make useof them yourself.