Updated on April 5, 2022
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.17
or go1.18
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:
- Differing requirements among projects — When switching between working on multiple projects, it is often necessary to use different Go versions for each.
- Creating specific test environments — When testing for backward compatibility or ensuring the success of bug fixes it is important to control for runtime version.
- 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.
- Go – https://golang.org/doc/install
- Git – https://git-scm.com/
- Modules – Using Go Modules
How to Work With Multiple Go Versions
We can use the go install
command to download install individual versions of Go.
Running go install golang.org/dl/go<version>@latest
will download and install a wrapper Go command for the specific Go version.
Note if the Go version you are running is lower than Go 1.16, use go get <package>
instead of go install <package>@latest
command for all the following examples.
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.18
$ go install golang.org/dl/go1.18@latest
$ go1.18 download
Using the wrapper go1.18
, we can build and test using Go v1.18. For example:
$ go1.18 mod init hello
go: creating new go.mod: module hello
$ echo 'package main; import "fmt"; func main() { fmt.Println("Hello, World") }' > hello.go
$ go1.18 build
$ ./hello
Hello, World
Another option is to use the version we just downloaded by setting the GOROOT
and add it to our path:
$ export GOROOT=$(go1.18 env GOROOT)
$ export PATH=${GOROOT}/bin;${PATH}
The default download directory is you <home>/sdk
folder. To remove a specific version, simply delete the specific version directory.
There is also a specially tagged version, called gotip
that represents the latest version of Go from the development tree:
$ go install golang.org/dl/gotip@latest
$ 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:
- A small application per Go version – example:
go1.18/main.go
- An internal package that implements the Go wrapper functionality:
internal/version/version.go
- 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.18")
}
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):
- Download – running
go<version> download
- Wrapper for Go toolchain – running
go<version> <anything else>
Download
An explicit version (ex: go1.18) 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 wrapper 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:
- Working with Embed in Go 1.16
- In-process caching in Go: scaling lakeFS to 100k requests/second
- Improving Postgres Performance Tenfold Using Go Concurrency
Table of Contents