Go Modules in Action

In Golang there was no official approach of how to manage dependency. Hence, in the early days of golang until recently, there were different ways in managing dependencies. In the beginning there is go get. It help to download package from the internet to your local directory which can be imported into your source code. Go get doesn't have any knowledge of version. It is always try to download the latest. Then, there are some attempts to manage dependency such as glide, dep and some others. Dep is an official proof of concept where go core team try to find the best solution of managing dependencies. Glide and dep after all are very similar with other package manager in different platform such as npm, cargo and also pipenv. But at the end, go module, which started as proof of concept vgo, comes as the official dependency management tool for Golang.

There was a time when I was slightly feel annoyed with Golang introducing different approaches of dependency management in very short period of time. I was starting a project using glide. Then I migrated it to dep, because I was told, it is the official approach. Only then I learned, go moved to go module. It only makes sense after I watched very clear explanation of Russ Cox in Gophercon 2018. The reason of introducing go module is to have build tool with this principles:

  1. Compatibility
  2. Repeatability
  3. Cooperation

Russ Cox  has also written multiple blog posts in elaborating the arguments which culminate in the official blog post in the Golang blog.

This blog post doesn’t try to repeat what that has clearly explained in documentation, the original blog post and Russ Cox's blog and video. I just want to synthesize my understanding and try to give personal example so that I can internalize the reason and understand how go module works. Go module has preliminary support

Dependency

In the beginning your code will be simple without any 3rd party dependencies.

// main.go
package main

import (
  "fmt"
)

func main() {
  fmt.Println(greet())
}

func greet() string {
  return "Hello World"
}

‌Now let's extract this simple functionality into its own package within the same source three. The structure will be look like following.

user@machine: ~/go/src/github.com/lamida/gomoddemo $ tree
.
├── cmd
│   └── main.go
└── greeter
    └── greeter.go
// greeter.go
package greeter
func Greet() string {
  return "Hello There!"
}
// main.go
package main
import (
  "fmt"

  "github.com/lamida/gomoddemo/greeter"
)
func main() {
  fmt.Println(greeter.Greet())
}

Publish The Package

As example, we want to extract the Greet() functionality into its own golang package. Of course in real world, there is a very rare occasion to have too simple library like this. We want to use this just for example.

user@machine: ~/go/src/github.com/lamida/gomoddemolib $ tree
.
└── greeter
    └── greeter.go

What we will do is just pushing the greeter package into its own git repo.

After that, we also can delete that gomoddemolib above project from our local disk. Please note that when publishing the package to git repo, there is no need to make the package as go modules package. For our example, one greeter.go file under greeter folder is enough.

At the same time, we will remove package greeter from gomoddemo project. Hence the project structure now just become like this.

user@machine: ~/go/src/github.com/lamida/gomoddemo$ tree
.
└── cmd
    └── main.go

In the main.go we have to make sure to import the greeter from the new path.

// main.go
package main

import (
  "fmt"
  // this import is from the new repo
  "github.com/lamida/gomoddemolib/greeter"
)

func main() {
  fmt.Println(greeter.Greet())
}

If we try to build this, we will get error like this.

user@machine: ~/go/src/github.com/lamida/gomoddemo $ go build
main.go:6:2: cannot find package "github.com/lamida/gomoddemolib/greeter" in any of:
        /snap/go/3739/src/github.com/lamida/gomoddemolib/greeter (from $GOROOT)
        /home/lamida/go/src/github.com/lamida/gomoddemolib/greeter (from $GOPATH)

That happens because gomoddemo project can't find dependency to gomoddemolib since we have deleted that from our local disk.

We need to enable go modules for gomoddemo so that go can manage the dependency for us.

user@machine: ~/go/src/github.com/lamida/gomoddemo $ go mod init github.com/lamida/gomoddemo
go: creating new go.mod: module github.com/lamida/gomoddemo

Let's see what is inside go.mod file.

// go.mod
module github.com/lamida/gomoddemo

go 1.12

It is just module declaration and go version. Next we can run go build for the main package under cmd folder.

user@machine: ~/go/src/github.com/lamida/gomoddemo $ cd cmd
user@machine: ~/go/src/github.com/lamida/gomoddemo $ go build
go: finding github.com/lamida/gomoddemolib/greeter latest
go: finding github.com/lamida/gomoddemolib latest
user@machine: ~/go/src/github.com/lamida/gomoddemo $ ./cmd
Hello from module

go build command automatically scan import in our codes and download the dependency directly. When we execute the binary named cmd under the cmd folder, it shows the output as what we import from the external package.

go.sum

After running go build, we also discover that there is a new file added in the root folder of the project.

//go.sum
github.com/lamida/gomoddemolib v0.0.0-20190518042331-b7c7cf7d5429/go.mod h1:ql3STdRmz950+yGEEso1wDner5pLOa1keXBZ77ZkT0w=

As we can read from golang wiki go.sum serves this function:

For validation purposes, go.sum contains the expected cryptographic checksums of the content of specific module versions.

To test the checksum, let's modify the sum by appending it with some random text. We can do so by using go mod verify command.

user@machine: ~/go/src/github.com/lamida/gomoddemo $ go mod verify
verifying github.com/lamida/gomoddemolib@v0.0.0-20190518042331-b7c7cf7d5429/go.mod: checksum mismatch
        downloaded: h1:ql3STdRmz950+yGEEso1wDner5pLOa1keXBZ77ZkT0w=
        go.sum:     h1:ql3STdRmz950+yGEEso1wDner5pLOa1keXBZ77ZkT0wX=

Updating The Package

The library we are importing are coming without any version metadata. Hence go module just pulls the latest commit from the master branch. Let's test to update the gomoddemolib another commit to the master branch. I am updating the greet message from "Hello from module" to "Hello from module updated". If we run go build after that, nothing is changed.

Updating Patch Version

The most easiest way to update this is to update release tag to gomodlib.

user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git tag v0.0.1
user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git push origin v0.0.1
Total 0 (delta 0), reused 0 (delta 0)
To github.com:lamida/gomoddemolib
 * [new tag]         v0.0.1 -> v0.0.1

Next we update the version in go.mod in gomoddemo project to point to this new version. But instead of pointing to the specific patch version, we will just set it to major version v0.

// go.mod
module github.com/lamida/gomoddemo

go 1.12

require github.com/lamida/gomoddemolib v0

Running go build will fetch the latest version.

user@machine: ~/go/src/github.com/lamida/gomoddemo/cmd $ go build
go: finding github.com/lamida/gomoddemolib v0.0.1
go: downloading github.com/lamida/gomoddemolib v0.0.1
go: extracting github.com/lamida/gomoddemolib v0.0.1
user@machine: ~/go/src/github.com/lamida/gomoddemo/cmd $ ./cmd
Hello from module updated

We can push another patch version from gomodlib.

user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git tag v0.0.2
user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git push origin v0.0.2
Total 0 (delta 0), reused 0 (delta 0)
To github.com:lamida/gomoddemolib
 * [new tag]         v0.0.2 -> v0.0.2

Running go build will automatically fetch another patch version.

Updating Minor Version

This also applicable if the library updates the minor version.

user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git tag v0.1.0
user@machine: ~/go/src/github.com/lamida/gomoddemolib $ git push origin v0.1.0
Total 0 (delta 0), reused 0 (delta 0)
To github.com:lamida/gomoddemolib
 * [new tag]         v0.1.0 -> v0.1.0

On running go build in the client code, the latest version of the dependency will be fetched.

user@machine: ~/go/src/github.com/lamida/gomoddemo/cmd $ go build
go: finding github.com/lamida/gomoddemolib v0.1.0
go: downloading github.com/lamida/gomoddemolib v0.1.0
go: extracting github.com/lamida/gomoddemolib v0.1.0

Updating Major Version

There are a different semantic for major version though. Let say after the library got stable, it releases v1 and then after that v2.

Updating to v1 is as simple as updating the go.mod to point to v1.

// go.mod
module github.com/lamida/gomoddemo

go 1.12

require github.com/lamida/gomoddemolib v1

However if you update to v2, go build will still fetch the latest update but the client code wouldn't directly run the v2 library.

user@machine: ~/go/src/github.com/lamida/gomoddemo/cmd $ go build
go: finding github.com/lamida/gomoddemolib v2.0.0+incompatible
go: downloading github.com/lamida/gomoddemolib v2.0.0+incompatible
go: extracting github.com/lamida/gomoddemolib v2.0.0+incompatible
$ ./cmd
Hello from module v1.0.0

We would have to enable the v2 by explicitly importing it in the client code.

// main.go
package main

import (
	"fmt"

	v2 "github.com/lamida/gomoddemolib/v2/greeter"
)

func main() {
	fmt.Println(v2.Greet())
}

user@machine: ~/go/src/github.com/lamida/gomoddemo/cmd $ go build
go: finding github.com/lamida/gomoddemolib v2.0.0+incompatible
go: downloading github.com/lamida/gomoddemolib v2.0.0+incompatible
go: extracting github.com/lamida/gomoddemolib v2.0.0+incompatible
$ ./cmd
Hello from module v2.0.0

If you saw error of module can't be found, make sure to update the version in the go.mod file to v2 and re-run go build or run command.

// go.mod
module github.com/lamida/gomoddemo

go 1.12

require github.com/lamida/gomoddemolib v2

The example concludes how we use go modules.

The code example used in this blog post is available in this git repo.