Playing With Go Modules
Update: much of this article has been rendered obsolete by changes made to Go modules since. Check this more recent post that’s up to date.
Had some free time in my hands, so I decided to check out the new Go modules. For those unaware, the next Go release (1.11) will include a new functionality that aims at addressing package management called “modules”. It will still be marked as experimental, so things may very well change a lot.
Here’s how it works. Let’s say we create a new package:
rselbach@wile ~/code $ mkdir foobar
rselbach@wile ~/code $ cd foobar/
Our foobar.go
will be very simple:
package foobar
import (
"fmt"
"io"
"github.com/pkg/errors"
)
func WriteGreet(w io.Writer, name string) error {
if _, err := fmt.Fprintln(w, "Hello", name); err != nil {
return errors.Wrapf(err, "Could not greet %s", name)
}
return nil
}
Notice we’re using Dave Cheney’s excellent errors
package. Until now, go get
would fetch whatever it found on that package’s repository. If Dave ever decided to change something drastic in the package, our code would probably break without us even knowing about it until too late.
That’s where Go modules come into play, as it will help us (1) formalize the dependency and (2) lock a particular version of it to our code. First we need to turn on the support for modules in our package. We do this by using the new go mod
command and giving out module a name:
$ go mod -init -module github.com/rselbach/foobar
go: creating new go.mod: module github.com/rselbach/foobar
After that, you will notice that a new file called go.mod
will be created in our package root. For now, it only contains the module name we gave it above:
module github.com/rselbach/foobar
But it does more than that, for now the go
command is aware that we are in a module-aware package and will behave accordingly. See what happens when we first try to build this:
$ go build
go: finding github.com/pkg/errors v0.8.0
go: downloading github.com/pkg/errors v0.8.0
It sees that we are using an external package and then it goes out, finds the latest version of it, downloads it, and adds it to our go.mod
file:
module github.com/rselbach/foobar
require github.com/pkg/errors v0.8.0
From now one, whenever someone uses our package, Go will also download version 0.8.0 of Dave’s errors package. If version 0.9.0 completely breaks compatibility, it will not break our code for it will continue to be compiled with correct version.
If we change go.mod
to require version 0.7.0, our next go build
will fetch it as well:
$ go build
go: finding github.com/pkg/errors v0.7.0
go: downloading github.com/pkg/errors v0.7.0
If you’re wondering where the packages are, they’re stored under $GOPATH/src/mod
:
$ ls -l ~/go/src/mod/github.com/pkg/
total 0
dr-xr-xr-x 13 rselbach staff 416 20 Jul 07:44 errors@v0.7.0
dr-xr-xr-x 14 rselbach staff 448 20 Jul 07:40 errors@v0.8.0
What about vendoring?
Good thing you asked. This is where I don’t like Go modules’ approach. It specifically aims at doing away with vendoring. Versioning is obviously taken care of by the modules functionality itself while disponibility is supposed to be taken care of by something like caching proxies.
I don’t see it. First of all, not everybody is Google. Maintaining cache proxies is extra work that many organizations don’t have the resources for. Also, what about when we’re working from home? Or on the commute? Sure, a VPN solves this, but then it’s one more thing we need to maintain.
That doesn’t mean you can’t vendor with Go modules. It’s simply not the default way it works. Let’s see how it works though. Let’s say we want to vendor our dependencies:
$ go mod -vendor
rselbach@wile ~/code/foobar $ ls -la
total 24
drwxr-xr-x 6 rselbach staff 192 20 Jul 08:01 .
drwxr-xr-x 33 rselbach staff 1056 20 Jul 07:14 ..
-rw-r--r-- 1 rselbach staff 252 20 Jul 07:22 foobar.go
-rw-r--r-- 1 rselbach staff 72 20 Jul 07:43 go.mod
-rw-r--r-- 1 rselbach staff 322 20 Jul 07:44 go.sum
drwxr-xr-x 4 rselbach staff 128 20 Jul 08:01 vendor
Notice how go mod
now has created a vendor
directory in our module root. Inside it you’ll find the source code for the modules we are using. It also contains a file with a list of packages and versions:
$ cat vendor/modules.txt
# github.com/pkg/errors v0.7.0
github.com/pkg/errors
We can now add this to our repository. But it’s not all done. Surprisingly, when modules support is enabled, the go
command will ignore our vendor
directory completely. In order to actually use our vendor directory, we need to explicitely tell go build
to use it
go build -v -getmode=vendor
This essentially emulates the behaviour of previous version of Go.