The latest release of Go, 1.11, was one of the largest in a long time when it comes to functionality. One of the headlines of this version were modules, which were very hyped and hated at the same time, since the community is divided into "modules team" and "dep team". While I feel that dep solves the problems I face on the daily basis, I decided to give the other approach a chance. Let me show you how you can build a small, sample application using "go mod".

Issue with GOPATH

One of the most important pitches for go modules was the idea of creating projects outside GOPATH. While this may seem strange to anyone using Go on a regular basis, but this really is a blocker for many people who don't do that regularly. If you, as a Go dev, need to fix some tiny thing in, say, JavaScript application, you git checkout the sources, make the change and push. You can even do that in /tmp directory so that your friends won't laugh at you being a React groupie now. With Go, you needed to configure special ENV variables, have the code in a specific place in your file system, etc...

Getting started

Even though our project can live outside GOPATH, we kinda need to trick the compiler into thinking that we're inside. To do that, we define an import path for our main package in main.go:

package main // import "github.com/mycodesmells/golang-examples/modules"

func main() {
    // ...
}

When we do that, we need to define our package as a module, using mod subcommand:

$ go mod init
go: creating new go.mod: module github.com/mycodesmells/golang-examples/modules

This produces a file, go.mod:

module github.com/mycodesmells/golang-examples/modules

At this point, though, we don't have any external dependencies, so we don't really get any benefits from go modules. Without them, we would still be able to compile and build such an app. So, let's add my favorite error handling library, pkg/errors by Dave Cheney and overcomplicate stuff:

package main // import "github.com/mycodesmells/golang-examples/modules"

import (
    "flag"
    "fmt"

    "github.com/pkg/errors"
)

func main() {
    name := flag.String("name", "", "name")
    flag.Parse()

    if err := greet(*name); err != nil {
        fmt.Printf("Failed to greet you: %v", err)
    }
}

func greet(name string) error {
    if name == "" {
        return errors.New("no name provided")
    }
    fmt.Printf("Hello %s! I'm not in the $GOPATH!\n", name)
    return nil
}

As you can see, we clearly are using some external dependency now. The problem is, that we did not define it anywhere in the .mod file. What happens if we try to build it?

$ go build .
$ ./modules -name MyCodeSmells
Hello MyCodeSmells! I'm not in the $GOPATH!

It works? Let's take a look at the .mod file:

module github.com/mycodesmells/golang-examples/modules

require github.com/pkg/errors v0.8.0

Impressive! The compiler was able to figure out that we indeed need pkg/errors package and suggested the version v0.8.0 which satisfies our needs. It even created another file go.sum with some additional information about the dependencies:

github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

Let's say that there is some unexpected behavior in v0.8.0 and we prefer to use v0.7.1? We can try modifying go mod and build it again, this time with -v flag for more verbose output:

$ go build -v .
go: finding github.com/pkg/errors v0.7.1
go: downloading github.com/pkg/errors v0.7.1
github.com/pkg/errors
github.com/mycodesmells/golang-examples/modules

What surprised me, was that go.sum now has information about both the currently used version, as well as the previous one:

github.com/pkg/errors v0.7.1 h1:0XSZhzhcAUrs2vsv1y5jaxWejlCCgvxI/kBpbRFMZ+o=
github.com/pkg/errors v0.7.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

By default, go mod does not delete references to the old, unused dependencies. If, however, you woudl delete go.sum and build again, the information about v0.8.0 is gone:

github.com/pkg/errors v0.7.1 h1:0XSZhzhcAUrs2vsv1y5jaxWejlCCgvxI/kBpbRFMZ+o=
github.com/pkg/errors v0.7.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

Where are the sources

While we're happy that the code works, it still is a bit too magical for us just to trust that everything is fine. After all, the sources have to live somewhere, right?

Indeed they are, the cache is located in $GOPATH/pkg/mod/ directory:

$ ls $GOPATH/pkg/mod/github.com/pkg
errors@v0.7.1 errors@v0.8.0

As you can see, both versions are stored there, each in their own directory.

Support for vendor

One last thing I (and probably many of you) was interested in, was, if the go modules support vendor/, so that your current toolchain can live with both mod and dep for example. Fortunately, the support is here, as you can move the sources of your dependencies to your projects directory with mod vendor subcommand:

$ go mod vendor
$ ls vendor
github.com  modules.txt
$ ls vendor/github.com/pkg
errors

As you can notice, it also creates a vendor/modules.txt file, which describes how each entry in go.mod file corresponds to which subdirectory within vendor/:

# github.com/pkg/errors v0.7.1
github.com/pkg/errors

Summary

As you can see, building a tiny example with Go modules does not seem too hard. There are a couple of things you need to remember, but apart from that, I would say that building standalone applications should not cause you any additional hassle. You can now do that outside GOPATH which appeals to some, but I will probably still prefer to keep all of my Go code in the same place.