An early peek at Go modules.(20 July 2018) Update:Since the writing of this article, Go1.11 came out with experimental support for Go modules. Some of the commands were also renamed/removed so this blogpost is in some sense outdated. However the main concepts still stand. Intro In this article, we will take go modules(earlier on it had the codename vgo) for a spin. A module is a collection of related Go packages. Modules are the unit of source code interchange and versioning. With modules, you can now work outside of GOPATH and also version your code in such a way that go is aware of. At the time of writing this, we need to be using go compiled from master branch for us to be able to use go modules. So lets do that, We could clone go from master and compile it ourselves, but I won't do that; instead I'll use gimme which is a tool developed by TravisCI peeps to help in installing various go versions. The instructions on how to install gimme can be found here; But since I'm on OSX;
brew install gimme && gimme master
That installs gimme and then uses gimme to install Go from master branch.
Lets activate the newly installed go and check version
source ~/.gimme/envs/gomaster.env && go version
go version devel +d278f09333 Thu Jul 19 05:40:37 2018 +0000 darwin/amd64
What up now
I have a go package called
meli and we are going to
convert that to use go modules.
meli is a faster, smaller alternative to docker-compose(albeit with less features.) So lets clone meli
in a directory that
is outside GOPATH.
My GOPATH is at ~/go so we'll clone into ~/mystuff instead.
git clone git@github.com:komuw/meli.git ~/mystuff/meli && cd ~/mystuff/meli
run;
go mod -init
go: creating new go.mod: module github.com/komuW/meli
go: copying requirements from Gopkg.lock
the -init flag initializes and writes a new
go.mod to the current directory, in effect creating a new module rooted at the current
directory.
If you were using another dependency manager before, mod -nit will intialize the
go.mod file using that dependency manager's files. I was using dep as my dependency manager so
go mod
-init used that.
From what I understand, go mod "already supports reading
nine different legacy file formats (GLOCKFILE, Godeps/Godeps.json, Gopkg.lock,
dependencies.tsv,
glide.lock, vendor.conf, vendor.yml, vendor/manifest, vendor/vendor.json)" -
see
this comment by Russ Cox.
It's nice to see that, the Go team has put some thought into that.
Let's have a look at the
go.mod file it created;
module github.com/komuw/meli
require (
github.com/Microsoft/go-winio v0.4.8
github.com/docker/distribution v0.0.0-20170720211245-48294d928ced
github.com/docker/docker v1.13.1
github.com/docker/docker-credential-helpers v0.6.1
github.com/docker/go-connections v0.3.0
github.com/docker/go-units v0.3.3
github.com/pkg/errors v0.8.0
golang.org/x/net v0.0.0-20180712202826-d0887baf81f4
golang.org/x/sys v0.0.0-20180715085529-ac767d655b30
gopkg.in/yaml.v2 v2.2.1
)
All my dependencies that were listed in
Gopkg.lock have been added to
go.mod with their correct versions.
Notice though that under dep, meli depended on
github.com/docker/distribution
version
v2.6.2
However go mod added it with version v0.0.0-20170720211245-48294d928ced
That is called a pseudo-version, the second part(20170720211245) is the timestamp in UTC of the commit
hash 48294d928ced.
The commit 48294d928ced is the commit corresponding to version v2.6.2,
see
here
Note: the pseudo versions are expected behaviour whilst a project is not yet a module (and its versions
is >=2)
Pretty neat, huh; but does it work?
Let's build the damn thing and see if it works(remember we are doing all these outside of GOPATH)
go build -o meli cli/cli.go && ./meli --help
Usage of ./meli:
-build
Rebuild services
-d Run containers in the background
-f string
path to docker-compose.yml file. (default "docker-compose.yml")
-up
Builds, re/creates, starts, and attaches to containers for a service.
-v Show version information.
-version
Show version information.
It works fine.
go.mod is not the only file created, a
go.sum file was also created.
cat go.sum
github.com/Microsoft/go-winio v0.4.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/docker/distribution v0.0.0-20170720211245-48294d928ced h1:/ybq/Enozyi+nBSAkL4j7vd+IBV6brrxB2srIO5VWos=
....
go.sum contains the expected cryptographic checksums of the content of specific module versions
The go command maintains a cache of downloaded packages(in $GOPATH/src/mod) and computes and records
the cryptographic checksum
of each package at download time.
The 'go mod -verify' command checks that the cached copies of module downloads still match both their
recorded checksums
and the entries in
go.sum
Lets check this crypto thing.
echo "Im a hacker" >> ~/go/src/mod/github.com/pkg/errors@v0.8.0/README.md
Then run;
go mod -verify
github.com/pkg/errors v0.8.0: dir has been modified (~/go/src/mod/github.com/pkg/errors@v0.8.0)
If you work in enterprise, this is the point at which you call in your Red team to
figure out who is messing
up with your packages.
Even though, we messed with the cached github.com/pkg/errors package, it doesnt stop us from building
our package.
go build -o meli cli/cli.go still works okay. I do not know if go build should complain if it
finds the
cached packages have been messed with, or whether it should redownload them afresh or just build the
package
as if nothing has happened(like it did.)
However, if you mess with the
go.sum file; go build fails with an error.
sed -i.bak "s/1NNxqwp/hackedHash/" go.sum && go build -o meli cli/cli.go
go: verifying gopkg.in/yaml.v2@v2.2.1/go.mod: checksum mismatch
downloaded: h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
go.sum: h1:hI93XBmqTisBFMUTm0b8Fm+jr3DghackedHash+5A1VGuI=
I'm liking the look of this crypto checksuming thing.
go mod has other flags that you can try out, run
go help mod to see them all. lets try the -sync flag which "synchronizes
go.mod with the source code in the module."
Synchronization of modules seems like something we might want to do, right?
go mod -sync
go: finding github.com/stretchr/testify/assert latest
go: finding github.com/stevvooe/resumable/sha256 latest
..
wait, why is it adding new packages?
It added new packages to
go.mod with a comment //indirect Let's see if the documentation can help us discover what is up
with
these //indirect thing.
go help mod | grep -i indirect -A 2 -B 2
Note that this only describes the go.mod file itself, not other modules
referred to indirectly. For the full set of modules available to a build,
use 'go list -m -json all'.
not useful, lets try go help modules instead. I do not know why the documentation is
spread between go help
mod and go help modules; but anyway;
go help modules | grep -i indirect -A 2 -B 2
... Requirements needed only for indirect uses are marked with a
"// indirect" comment in the go.mod file. Indirect requirements are
automatically removed from the go.mod file once they are implied by other
Okay, so the documentation seems to be saying that, for example in meli's case; although
meli does not use
github.com/stretchr/testify one of it's dependencies may be using it.
So which dependency of meli is using testify(meli doesnt use testify or any other non-stdlib testing
libraries)?
Because meli still vendors its dependencies, lets see if we can use grep to find out;
grep -rsIin testify .
./go.mod:14: github.com/stretchr/testify v1.2.2 // indirect
./go.sum:21:github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
./go.sum:22:github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
grep isn't helping, maybe go mod has a flag to give us this information?
go mod has a -graph flag which according to the documentation; "The -graph flag prints the module
requirement graph (with
replacements applied) in text form."
niice, looks like what we need.
go mod -graph
github.com/komuw/meli github.com/stevvooe/resumable@v0.0.0-20170302213456-2aaf90b2ceea
github.com/komuw/meli github.com/stretchr/testify@v1.2.2
github.com/komuw/meli golang.org/x/net@v0.0.0-20180712202826-d0887baf81f4
That's not very helpful, I still do not know which dependency introduced
github.com/stretchr/testify. I asked on the
#modules slack channel and
someone suggested I try go list; but
go list -test -deps | grep testify didn't help either.
Go modules and code contribution
One of the hardest things I've had before with Go is contributing to other peoples' projects.
Usually -in other languages- you would fork the project, make changes, run to make sure everything
works okay, then when
happy, open a pull request from your fork to the other project.
With Go however, I've had problems. I would fork a project and make changes; but when it came to
running the thing, things
would go haywire.
This is because import paths would still be pointing to the old project instead of mine. Note; this is
probably my own failing
rather than that of Go. If you think I've been doing it wrong, let me know.
I recently came across this tweet by
Francesc
Campoy and it has improved things for me, but it still felt odd.
Talking of Campoy, he has a fantastic
youtube
channel that is all things Go, If you have never checked out, do yourselve a favour.
Can go modules help us here? It turns out, they can! I guess.
meli depends on
github.com/docker/docker/client.
Let's say we wanted to add some feature to the docker client that we would later propose in a pull
request to docker. I forked
docker over to
https://github.com/komuw/moby
Now lets clone our fork into a directory that is outside GOPATH
git clone git@github.com:komuw/moby.git ~/mystuff/moby
The feature we want to add is; every time you declare a docker client(using
client.NewEnvClient() ), it should log the docker version that you are using.
Here's the change I made to the
NewEnvClient function
func NewEnvClient() (*Client, error) {
+ fmt.Println("\n\t You are using docker version:", api.DefaultVersion)
return NewClientWithOpts(FromEnv)
}
The full diff
can
be seen here
We have made changes to the docker client on our local clone at
~/mystuff/moby. How do we use that change in
meli before even pushing those changes to our fork(before even sending a pull request to
docker)?
go modules supports dependency replacement. The replacement can point to go code that is
anywhere(including in our machine.)
Add the following line to
go.mod
replace github.com/docker/docker v1.13.1 => ~/mystuff/moby
That is telling go to replace the docker dependency with a local dependency at the path
~/mystuff/moby
go mod -verify
go: errors parsing go.mod:
~/mystuff/meli/go.mod:20: replacement module without version must be directory path (rooted or starting with ./ or ../)
nice error message, let's comply and use relative paths; change the line in
go.mod to
replace github.com/docker/docker v1.13.1 => ../moby
go mod -verify
go: parsing ../moby/go.mod: open ~/mystuff/moby/go.mod: no such file or directory
go: error loading module requirements
This time around the error message is not that descriptive. Paul Jolly(who has been
doing an amazing job
answering go module related questions all over the internet),
mentioned
that "the new path should be a directory on the local system that contains a module"
So lets add a go.mod file to our clone of moby(docker)
echo 'module "github.com/docker/docker"' >> ~/mystuff/moby/go.mod && \
go mod -verify
all modules verified
This is looking good.
Lets rebuild meli to use our modified copy of docker.
go build -o meli cli/cli.go
go: finding github.com/gogo/protobuf/proto latest
go: finding github.com/gogo/protobuf v1.1.1
go: downloading github.com/gogo/protobuf v1.1.1
go: finding github.com/opencontainers/image-spec/specs-go latest
go: finding github.com/opencontainers/image-spec v1.0.1
go: downloading github.com/opencontainers/image-spec v1.0.1
# github.com/komuw/meli
./types.go:77:44: undefined: volume.VolumesCreateBody
./types.go:120:70: undefined: volume.VolumesCreateBody
./volume.go:16:3: undefined: volume.VolumesCreateBody
# github.com/docker/docker/client
../moby/client/container_commit.go:17:15: undefined: reference.ParseNormalizedNamed
../moby/client/container_commit.go:25:9: undefined: reference.TagNameOnly
../moby/client/container_commit.go:30:16: undefined: reference.FamiliarName
../moby/client/image_create.go:16:14: undefined: reference.ParseNormalizedNamed
../moby/client/image_create.go:22:25: undefined: reference.FamiliarName
../moby/client/image_import.go:18:16: undefined: reference.ParseNormalizedNamed
../moby/client/image_pull.go:23:14: undefined: reference.ParseNormalizedNamed
../moby/client/image_pull.go:29:25: undefined: reference.FamiliarName
../moby/client/image_pull.go:59:8: undefined: reference.TagNameOnly
../moby/client/image_push.go:19:14: undefined: reference.ParseNormalizedNamed
../moby/client/image_push.go:19:14: too many errors
Looks like I picked a hard one. I'm guessing that I'm running into issues that go much
deeper than just
go modules?? Maybe?
My speculation is that the way docker uses import path comments of the form
package client // import "github.com/docker/docker/client"
see here
, is muddying things.
So probably, even though I have added a replace statement
github.com/docker/docker v1.13.1 => ../moby
the code in
../moby still has import comments
// import "github.com/docker/docker/client"
that makes go try and use
github.com/docker/docker which is what we wanted to replace in the first place??
I'm just speculating here, there's a good probability that there's something I'm overlooking.
I'll try and open an issue with the go repo(or ask on #modules slack channel) sometime later if just to
satisfy my curiosty.
Go modules and code contribution, take II
Even though I was not able to use a local copy of docker, I was able to carry out the same procedure
with another package.
Lets undo the replace directive we had in
go.mod
We are going to try and add
github.com/pkg/errors as a dependency to meli, we will fork it(pkg/errors), clone it outside
GOPATH,
make changes to it and try to use our local copy.
here's the change I made to
meli to use
github.com/pkg/errors for error handling;
import "github.com/pkg/errors"
func main() {
err := errors.New("whoops useless error")
fmt.Printf("%v", err)
....
}
The full diff
can
be seen here
run
go mod -sync so that those changes get picked up. When you do that,
github.com/pkg/errors v0.8.0 is added as a direct dependency.
Lets fork errors package to
https://github.com/komuw/errors
and clone it locally to a path outside GOPATH ie
~/mystuff/errors
Then we add a replacement directive in
meli's
go.mod
replace github.com/pkg/errors v0.8.0 => ../errors
of course we also add a go.mod file to the local errors package
echo 'module "github.com/pkg/errors"' >> ~/mystuff/errors/go.mod
As more packages/modules add
go.mod files to their repo's we won't have to do this. I wish go mod would be able to
automatically add
a
go.mod file for you, but it seems like per
this
comment from Russ Cox that it is not possible to do so without unwanted side effects?
Let's modify the local errors package. We want to modify the
errors.New function to log something when called. The change I made is;
func New(message string) error {
fmt.Println("\n\t hello new called.")
return &fundamental{
msg: message,
stack: callers(),
}
}
The full
diff
is here
Lets build meli and run it.
go build -o meli cli/cli.go && ./meli --help
hello new called.
whoops useless errorUsage of ./meli:
-build
Rebuild services
Look ma, We done made it.
meli is now using
github.com/pkg/errors for error handling, but we are using a local copy of our fork of
github.com/pkg/errors
Pretty neat if you ask me.
Conclusion
I like the direction that go modules is taking. Not all t's are tied and i's dotted; At the time of
writing this, there are
about
40
open issues
concerning go modules. I expect that number to rise as more people try out go modules.
go modules is expected to ship with Go version 1.11(eta ~August) as an experimental feature.
go modules will also make it possible for Go to download packages from a registry of your choice(à la
npm, pypi etc).
Have a look at
https://github.com/gomods/athens,
which is an upcoming package registry and proxy server for Go
Shout out to Paul Jolly, Jeff Wendling, Bryan C. Mills, Russ Cox among others who have had to do a lot
of hand holding all
over the internet on go modules related issues/questions.
Special shout out to Sam Boyer for his critique(in the literary sense) of go modules(links below.)
Related readings:
1.
Go & Versioning, by Russ
Cox.
2.
Taking
Go modules for a spin, by Dave Cheney
3.
Thoughts on vgo and dep,
by Sam Boyer
4.
An Analysis of Vgo, by Sam
Boyer
5.
Project
Athens, by Aaron Schlesinger