Skip to content

Instantly share code, notes, and snippets.

@rogpeppe
Created November 27, 2018 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rogpeppe/2bb7c0f90055da52544d87c4d3e3c58d to your computer and use it in GitHub Desktop.
Save rogpeppe/2bb7c0f90055da52544d87c4d3e3c58d to your computer and use it in GitHub Desktop.

Go Modules Workshop

The aim of this workshop is to familiarise you with the basics of Go modules. By the end, you should know how to:

  • build a project that uses modules
  • create a project that uses modules
  • keep module dependencies up to date
  • manage module dependencies, exclusions and replacements
  • deal with dependencies that don't use Go modules
  • have understanding of some external Go module tools.

Prerequisites

You should have a computer with Go 1.11, git installed on it, and a github account.

Note that all the information below can be found in the go command help pages. Specifically, see:

go help modules
go help go.mod
go help mod
go help list
go help module-get

Exercise 1: Build a project with modules

First, find a project that supports modules; then use the Go command to build it. Note that you should not be inside your $GOPATH directory.

For example:

git clone https://github.com/myitcv/gobin
cd gobin
go build
./gobin --help

Exercise 2: Create a project as a Go module

Make a directory for the module:

mkdir /tmp/testmodule
cd /tmp/testmodule

Create the module:

go mod init github.com/myname/testproject

Write a Go file that imports something:

package main

import (
	"fmt"
	"log"

	"gopkg.in/yaml.v2"
)

func main() {
	data, err := yaml.Marshal(map[string]string{
		"some yaml": "values\netc",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s", data)
}

Build the program:

go build

Run the binary:

./testproject

Now look at the go.mod file. It has been automatically changed to reflect the dependencies in use.

cat go.mod

Exercise 3: exploring dependencies

You can list dependencies with go list.

To list all the dependencies that the project has (this recursively includes testing dependencies too):

go list all

There is also a module variant of the go list command. To list all modules in the current project:

go list -m all

The go mod subcommand provides some useful tools for exploring why module dependencies are there.

For example, you might be wondering why some particular package was listed by the go list all command above. The go mod why command is your friend. It prints the shorted dependency path leading to a given package.

go mod why text/template

If you want to know why some particular module (as opposed to package) is used:

go mod why -m gopkg.in/check.v1

The go mod graph command shows the entire dependency graph, including modules that aren't actually in use but were used to arrive at the current dependency graph.

Exercise 4: go get for updating dependency versions

If we wish to update a dependency version, we can change the go.mod file directly. But it's generally easier to use the go get command to do that.

For example, to use an earlier version of the gopkg.in/yaml.v2 repository, we could run:

go get gopkg.in/yaml.v2@v2.2.0

Try this, and see how this changes the go.mod file.

Exercise 4: Semantic versions

Semantic versions (described at https://semver.org) are the cornerstone of the Go module system. It's useful to understand how semantic versions work before diving into modules.

What's a semantic version? In the simplest case:

vMAJOR.MINOR.PATCH

where MAJOR, MINOR and PATCH are numbers. For example:

v2.3.4

Modules are expected to retain compatibility within the same major version (with some exceptions).

In the general form, a semantic version can also include a pre-release version and a patch version:

vMAJOR.MINOR.PATCH-PRE+PATCH

For example:

v2.3.4-alpha+amd64

Semantic versions have a well defined ordering. MAJOR, MINOR and PATCH are compared in precedence order. If PRE is present, the version compares earlier. PATCH is ignored when comparing. PRE may contain multiple parts separated by dots; earlier parts take precedence over later parts, and parts consisting entirely of digits are compared numerically; numeric parts always compare before non-numeric parts.

Rearrange the following semantic versions in order:

v1.2.1-foo.5
v1.2.1-foo.12
v1.0.0
v1.1.0
v2.0.1
v1.2.1-foo.beta
v1.2.1-foo
v1.2.1-foo.0alpha
v1.2.1-foo-beta
v1.2.2
v1.2.1
v1.2.0
v1.2.1-foo.alpha

Exercise 5: Pseudo versions

Sometimes we'll want to use a dependency that hasn't yet been tagged with a version. The Go tool uses pseudo-versions to represent dependencies in this case.

Some examples:

  • a module might not have any tagged versions at all
  • you want to use the tip version of a package
  • you want to use some other branch of a package

A pseudo-version is just another semantic version except that it uses the PRE part of the version (the prerelease part) to represent the commit. A pseudo-version comes in one of three flavours. The first is used when there are no commits earlier than the required version. It holds the date of the commit followed by its commit hash:

v1.2.3-20181128-59aa596

The second is used when there is already a tagged version before the required version (note that

v1.2.4-0.20181128-59aa596

If the most recent commit before the required version already has a PRE section, the pseudo-version will look like this:

v1.2.3-PRE.0.20181128-59aa596

Although it's useful to know how these work, we almost never have to write out a pseudo-version by hand, because the go tool does it for us.

Try this out with the github.com/rogpeppe/module-workshop repository. Add a call to workshop.Version (it just returns a string identifying the version) to your code, and the relevant import statement.

Then experiment with using go get github.com/rogpeppe/module-workshop@$version for different versions (there are tags for all the versions in the above Semantic Versions second).

Exercise 6: Major version changes

When there's a major version change (except major versions 0 and 1), the import path for a module changes.

This means it's possible to have two major versions of a module loaded at the same time.

There is already a v2 version of github.com/rogpeppe/module-workshop available. Change your import to import from github.com/rogpeppe/module-workshop/v2 and try to compile the code. Then fix the code.

Experiment with importing both the v1 version and the v2 version.

Exercise 7: gohack and the replace statement

It's common to need to experiment on external code; this was easy when there was a global mutable GOPATH directory, but less so when all modules are read-only. The gohack command tries to make this straightforward again.

First use your new go modules-fu to build the gohack command. It's in github.com/rogpeppe/gohack.

You can use gohack to copy a temporary copy of any module to your gohack directory ($HOME/gohack by default). For example:

gohack get github.com/rogpeppe/module-workshop

Run this command and take a look at your go.mod file. Observe that it's added a replace clause.

Experiment with editing the checked out code and recompiling your program.

You can drop the replace statement with

gohack undo github.com/rogpeppe/module-workshop

or just

gohack undo

to undo all replace statements that refer to local directories.

Exercise 8: tidy

Although the go command adds new dependencies automatically, it doesn't remove them. It's always good practice to run the go mod tidy command to do that before committing.

go mod tidy

Try that and see what difference it makes to your go.mod file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment