Barak Amar
May 12, 2021

As a user of the Go programming language, I’ve found it useful to enable the running multiple versions within a single project. If this is something you’ve tried or have considered, great!

In this post I’ll present the when and the how of enabling multiple Go versions. Finally, we’ll conclude with a discussion of why this approach is so powerful.

Let’s go!

When Do We Need Multiple Go Versions?

Out of the box, installing Go means you have a single go command you can run to build and test your project. This is simple for getting started, but also can be limiting.

A more flexible set up is to enable running multiple versions within the same environment via go1.15 or go1.16 commands, for example. An alternative approach involves setting your terminal’s PATH environment variable to point to a specific Go version’s SDK.

There are a few scenarios where I’ve found it beneficial to have more than one version available:

  1. Differing requirements among projects — When switching between working on multiple projects, it is often necessary to use different Go versions for each.
  2. Creating specific test environments — When testing for backward compatibility or ensuring the success of bug fixes it is important to control for runtime version.
  3. Staying on the bleeding edge — When testing the behavior new features or packages only available in the latest Go release.

Prerequisites

This guide assumes you have the knowledge of how to build and run programs using Go. Specifically, this means you have Go and Git installed and available in your path.

  1. Go – https://golang.org/doc/install
  2. Git – https://git-scm.com/
  3. Modules – Using Go Modules

How to Work With Multiple Go Versions

We can use the go get command to fetch individual versions of Go.

Running go get golang.org/dl/go<version> will download and install a wrapper Go command for the specific Go version.

By using the Go wrapper we can download the specific version of Go and run the Go tool-chain for this release.

Example for Go v1.16.4

$ go get golang.org/dl/go1.16.4
$ go1.16.4 download

Using the wrapper go1.16.4, we can build and test using Go v1.16.4. For example:

$ go1.16.4 mod init hello
go: creating new go.mod: module hello
$ echo 'package main; import "fmt"; func main() { fmt.Println("Hello, World") }' > hello.go
$ go1.16.4 build
$ ./hello
Hello, World

Another option is to use the version we just downloaded and set the GOROOT. This adds the specific version to your path:

$ export GOROOT=$(go1.16.4 env GOROOT)
$ export PATH=${GOROOT}/bin;${PATH}

The GOROOT value above is also the folder you can delete to uninstall a specific version.

There is also a specially tagged version, called gotip that gets the latest version of Go from the development tree:

$ go get golang.org/dl/gotip
$ gotip download

Note that gotip downloads and builds the current development version. It can also accept additional parameters – branch name or change list (CL) and use it to pull the specific Go version.

How It Works Under the Hood

The repository https://go.googlesource.com/dl holds the magic for this functionality. It features:

  1. A small application per Go version – example: go1.16.4/main.go
  2. An internal package that implements the Go wrapper functionality: internal/version/version.go
  3. A helper application to generate the Go wrapper code: internal/genv/main.go

For each version of Go the main.go, for the Go wrapper looks like:

package main

import "golang.org/dl/internal/version"

func main() {
 version.Run("go1.16.4")
}

As you can see there isn’t much going on. We simply call the common code for the version to be served.

A packaged version’s Run function performs the two main tasks (based on command line arguments):

  1. Download – running go<version> download
  2. Wrapper for Go toolchain – running go<version> <anything else>

Download

An explicit version (ex: go1.16) fetches an archive with the Go SDK from https://dl.google.com/go based on the version, OS, and platform to a folder under your <home folder>/sdk.

Next, it verifies the archive integrity using sha256, by fetching a checksum file from the download server. Finally, it unpacks the archive and creates an empty marker file called `.unpacked-success`

Gotip version – When using gotip download we only have the source code to work with – latest or specific branch / change list (if specified)

gotip: usage: gotip download [CL number | branch name]

Gotip download will use git to fetch the source files, perform a cleanup, and run the relevant make script to build Go.

Unlike when unpacking a specific distribution, we do not have a marker file to mark unpacked success. Instead, gotip will always try to fetch any update and run the build process.

Wrapper

The wrapper functionality first verifies we have a valid SDK in place by verifying we have a maker file (.unpacked-success) under the relevant Go folder.

An error message will guide you to run a download request in case it is missing – go<version>: not downloaded. Run 'go<version> download' to install to <home>/sdk/go<version>. The following code (from internal/version/version.go), formats the command to execute, based on the go version we like to execute. It renders the environment with GOROOT – set to the correct path (and also adds it to the PATH).

func runGo(root string) {
 gobin := filepath.Join(root, "bin", "go"+exe())
 cmd := exec.Command(gobin, os.Args[1:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 newPath := filepath.Join(root, "bin")
 if p := os.Getenv("PATH"); p != "" {
 newPath += string(filepath.ListSeparator) + p
 }
 cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))

 handleSignals()

 if err := cmd.Run(); err != nil {
 os.Exit(1)
 }
 os.Exit(0)
}

In order to have a Go wapper main program for each version an helper command – genv is used. 

It accepts the version of go which we like to build a Go wrapper as an input.

First it runs go list -m -json and parses the output:

$ go list -m -json
{
        "Path": "golang.org/dl",
        "Main": true,
        "Dir": "<workspace>/dl",
        "GoMod": "<workspace>/dl/go.mod",
        "GoVersion": "1.11"
}

Then it matches the Path to golang.org/dl and uses the Dir as the target directory for the wrapper code (`<version>/main.go`).

Finally, it uses the available Go template to render the Go wrapper code reviewed above.

Summary

The Go solution for running multiple Go versions is simple and elegant – it uses the same tools as other things in Go. And supplies a way for us to access different versions of the Go SDKs with minimal coding.


To see an example project leveraging this workflow, check out the lakeFS GitHub repository!

More on how lakeFS uses Go:

LakeFS

  • Get Started
    Get Started