Working with Go Modules

July 26, 2018

go

In my previous post on Sending Messages to the Azure IoT Hub, I promised that my follow-up post would continue with that example and show how to route device messages to other Azure services.

However, as Go 1.11 beta 2 has just been released, with support for modules, I thought I’d put together a short post on how this changes my Go workflow.

Updated for the final release of Go 1.11

The go command currently supports (as at v1.11) handling dependencies either as modules or in the traditional way as part of the $GOPATH. It distinguishes between these two methods by checking where your code is stored. Code under $GOPATH/src will be treated in the traditional way, with dependencies being pulled from within the $GOPATH. Code outside of $GOPATH/src will use the new module functionality to manage dependencies. See Russ Cox’s post for full details on the changees to the go command.

I’ll go through two examples of how the new module feature changes the way I now work, one with a new empty project and one with an existing project that was previously built using dep.

New Project

To start a new project, I created a new folder (outside of $GOPATH/src) and changed into it. Then I ran the go mod command with the init subcommand flags. The init subcommand tells go mod to generate a new go.mod file that will manage the dependencies of the project. This is followed by the name of the module, in this case “github.com/tophatsteve/modexample”.

mdkir modexample
cd modexample
go mod init github.com/tophatsteve/modexample

Running these commands gives the following output:

go: creating new go.mod: module github.com/tophatsteve/modexample

At this point the go.mod file will only contain the module name as follows:

module github.com/tophatsteve/modexample

For the actual code in this project, I just created a simple web server with one external dependency, Gorilla Mux, as follows:

package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
)

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/", homeHandler)
	http.ListenAndServe(":8383", r)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "Success\n")
}

To add a specific version of a dependency, we just use go get and append the version number onto the dependency path after an @. If you just want to use the latest version of a dependency then you do not need to specify the version. For this project I installed version v1.6.2 of Gorilla Mux by running the following command:

go get github.com/gorilla/mux@v1.6.2

which downloaded Gorilla Mux v1.6.2 to the local module cache, produced the following output:

go: finding github.com/gorilla/mux v1.6.2
go: downloading github.com/gorilla/mux v1.6.2

and added the following line to go.mod file:

require github.com/gorilla/mux v1.6.2

The project can then be built and run using go build or go run as usual.

If you don’t want specific versions of your dependencies, and are happy to use the latest versions, you can completely skip the go get step. Running go build or go run will automatically download the latest versions of any dependencies and update the go.mod file to reflect these versions.

Existing Project with dep

If you have an existing project that uses another dependency manager, such as dep, or glide, converting over to use modules has been made incredibly simple. The go mod -init command will read an existing dependency manager file and create a go.mod file that contains all the same dependencies.

As an example I used my project saas from https://github.com/tophatsteve/saas.

git clone https://github.com/tophatsteve/saas.git
cd saas

This project uses dep to manage dependencies so it has a Gopkg.toml file that specifies explicit dependencies and an automatically generated Gopkg.lock file that species all dependencies, implicit and explicit. The contents of Gopkg.toml are:

[[constraint]]
  branch = "master"
  name = "github.com/golang/protobuf"

[[constraint]]
  name = "github.com/stretchr/testify"
  version = "1.1.4"

[[constraint]]
  branch = "master"
  name = "golang.org/x/net"

[[constraint]]
  name = "google.golang.org/grpc"
  version = "1.8.2"

and for Gopgk.lock:

# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.


[[projects]]
  name = "github.com/davecgh/go-spew"
  packages = ["spew"]
  revision = "346938d642f2ec3594ed81d874461961cd0faa76"
  version = "v1.1.0"

[[projects]]
  branch = "master"
  name = "github.com/golang/protobuf"
  packages = [
    "proto",
    "ptypes",
    "ptypes/any",
    "ptypes/duration",
    "ptypes/timestamp"
  ]
  revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"

[[projects]]
  name = "github.com/pmezard/go-difflib"
  packages = ["difflib"]
  revision = "792786c7400a136282c1664665ae0a8db921c6c2"
  version = "v1.0.0"

[[projects]]
  name = "github.com/stretchr/testify"
  packages = ["assert"]
  revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
  version = "v1.1.4"

[[projects]]
  branch = "master"
  name = "golang.org/x/net"
  packages = [
    "context",
    "http2",
    "http2/hpack",
    "idna",
    "internal/timeseries",
    "lex/httplex",
    "trace"
  ]
  revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"

[[projects]]
  branch = "master"
  name = "golang.org/x/text"
  packages = [
    "collate",
    "collate/build",
    "internal/colltab",
    "internal/gen",
    "internal/tag",
    "internal/triegen",
    "internal/ucd",
    "language",
    "secure/bidirule",
    "transform",
    "unicode/bidi",
    "unicode/cldr",
    "unicode/norm",
    "unicode/rangetable"
  ]
  revision = "eb22672bea55af56d225d4e35405f4d2e9f062a0"

[[projects]]
  branch = "master"
  name = "google.golang.org/genproto"
  packages = ["googleapis/rpc/status"]
  revision = "a8101f21cf983e773d0c1133ebc5424792003214"

[[projects]]
  name = "google.golang.org/grpc"
  packages = [
    ".",
    "balancer",
    "balancer/roundrobin",
    "codes",
    "connectivity",
    "credentials",
    "encoding",
    "grpclb/grpc_lb_v1/messages",
    "grpclog",
    "internal",
    "keepalive",
    "metadata",
    "naming",
    "peer",
    "resolver",
    "resolver/dns",
    "resolver/passthrough",
    "stats",
    "status",
    "tap",
    "transport"
  ]
  revision = "e687fa4e6424368ece6e4fe727cea2c806a0fcb4"
  version = "v1.8.2"

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "82c21d66664fc0c6eec252e6d7f6d5ff11dec519184bc0f0a5ccd65594c289f4"
  solver-name = "gps-cdcl"
  solver-version = 1

Initialising the project builds the go.mod file by reading the dependencies from the Gopkg.lock file.

go mod init github.com/tophatsteve/saas

produces the following output

go: creating new go.mod: module github.com/tophatsteve/saas
go: copying requirements from Gopkg.lock

and generates the following go.mod file

module github.com/tophatsteve/saas

require (
	github.com/davecgh/go-spew v1.1.0
	github.com/golang/protobuf v0.0.0-20171113180720-1e59b77b52bf
	github.com/pmezard/go-difflib v1.0.0
	github.com/stretchr/testify v1.1.4
	golang.org/x/net v0.0.0-20171212005608-d866cfc389ce
	golang.org/x/text v0.0.0-20171218113626-eb22672bea55
	google.golang.org/genproto v0.0.0-20171212231943-a8101f21cf98
	google.golang.org/grpc v1.8.2
)

This has now converted the project over to using modules and the Gopkg.toml and Gopkg.lock files can safely be deleted.

Caveat Emptor

This post was based on how the module functionality works in Go 1.11 and may change in the future. It is also worth noting that the module feature is experimental and may be replaced in the future with a different way of managing dependencies, so it is probably not a good idea to rely on it for production software.

There is a lot more to go modules than I have covered in this post, for full details see Russ Cox’s post on the golang-dev forum.

Copyright (c) 2018, all rights reserved.