Go Environment Setup, Minus the Insanity
So you’re trying out the Go programming language. Good for you! It’s a neat language with a unique and, in my opinion, refreshing approach to clean design, concurrency natives, and more. That’s not what this article is about, though.
package X: cannot download, $GOPATH must not be set to $GOROOT. For more details see: ‘go help gopath’
The help text kind of helps, but only somewhat. Why is this stuff necessary? Well…
GOPATH and Coding by Convention
When designing any kind of system for other programmers to use, one has to decide what implied conventions the users should adhere to, and what the users should be able to configure (or generally control) themselves. Some, like gcc/g++, dump all the configuration in the developer’s lap. Between understanding the compiler, linker, dependency location and configuration, setup of a build system using GNU Make (or other similar tools), just building some software becomes a full-time job. Others, such as Python, are more “batteries included”: running stuff is trivial, installing dependencies is as simple as a single command, etc. The same applies for more abstract tools (consider Flask vs Ruby on Rails).
As a set of build tools, Go errs heavily towards the “convention over configuration” mindset. If you don’t follow the not-especially-flexible convention, things get ugly.
Go’s first convention: environment variables. The way these variables are defined varies by platform, but there are always two most important variables:
GOROOT
— this should point to the directory at which Go itself is installed; you should never have to mess with thisGOPATH
— this should point to your Go “workspace”, a special area where Go code, executables, and other files reside
The workspace is expected to be a directory containing three directories: bin
, pkg
, and src
. bin
will contain any executable binaries we compile using go install
, pkg
will have any non-executable binaries that get created, and src
is for all source code (project, dependencies… everything not bundled with Go itself). In particular, src
expects its subdirectories to follow the same structure that imports follow. For example, if your code used import "github.com/foo/bar"
then Go will interpret that as a request to look at $GOPATH/src/github.com/foo/bar/*.go
.
Note: I will be using Unix-style variables and commands (e.g.
$GOPATH
) but this information is valid for all platforms running Go.
Step by Step Go Setup
To see all this in action (and to demonstrate other aspects of setting up a Go environment), let’s look at a pre-built sample application: https://github.com/fsufitch/sample-go-app. It takes some input from the command line, and starts a web server serving a cat picture and an uptime counter.
These steps are written as I set all this up on a bare-bones AWS EC2 Ubuntu instance — as vanilla Linux as you can get.
Step 1: Install Go
To make sure we’re using the latest Go, we’ll be installing it manually. Don’t worry, it’s not intimidating at all. Just grab the appropriate archive from here, and unpack it: https://golang.org/dl/. In our AWS server’s case:
$ curl https://dl.google.com/go/go1.9.2.linux-amd64.tar.gz | tar xz
This gave us a go/
folder containing everything we need. We can even check if it works:
$ go/bin/go version
go version go1.9.2 linux/amd64
Looks good. We’ll get tired of typing go/bin/go
though, so let’s add it to our PATH
:
$ echo "export PATH=~/go/bin:$PATH" >> ~/.bashrc
After restarting the terminal (to apply the new .bashrc
), we should be able to use the go
command line easily.
Step 2: Set up the workspace
As mentioned before, we need a dedicated “workspace” where all our source code will reside. Let’s call it something simple, and add it to our environment. We’ll add the executables in our Go workspace to our path as well.
$ mkdir go-workspace
$ echo "export GOPATH=~/go-workspace" >> ~/.bashrc
$ echo "export PATH=~/go-workspace/bin:$PATH" >> ~/.bashrc
We can double check we got it right by asking Go for its value (after restarting the terminal again):
$ go env GOPATH
/home/ubuntu/go-workspace
Looking good!
Step 3: Install our package
We’re jumping right into it! Go can install stuff right from Github… and it even uses Git to get the files. Given the sample app’s URL (https://github.com/fsufitch/sample-go-app), we can just run this (including -v
to see what it’s actually doing):
$ go get -v github.com/fsufitch/sample-go-app
github.com/fsufitch/sample-go-app (download)
github.com/gorilla/mux (download)
github.com/urfave/cli (download)
github.com/gorilla/mux
github.com/fsufitch/sample-go-app/server
github.com/urfave/cli
github.com/fsufitch/sample-go-app
Looks like it downloaded three packages, and installed four (since one repository contained two). We can even see our files ended up in the right place:
$ ls go-workspace/src/github.com/fsufitch/sample-go-app/
glide.lock glide.yaml LICENSE main.go README.md server static
Step 4: Run it!
Go helpfully put the compiled binary for the sample app in the bin
of the workspace. Since that’s on our PATH
, we can simply run it (using the static resource directory provided with our repository):
$ sample-go-app go-workspace/src/github.com/fsufitch/sample-go-app/static/
2018/01/16 21:46:50 Sending port and static path to server module (8080, go-workspace/src/github.com/fsufitch/sample-go-app/static/)
2018/01/16 21:46:50 Creating router using static path: go-workspace/src/github.com/fsufitch/sample-go-app/static/
2018/01/16 21:46:50 Creating uptime handler; unix now: 1516139210
2018/01/16 21:46:50 Serving on :8080...
It works!
You can even check out the two endpoints of the server:
So… we’re done, right? Not quite.
Did you notice how, when we installed the package, it installed its two dependencies? Go is smart: it traverses all the source it’s compiling, and automatically downloads anything needed. It’s not smart enough, though: it will only download the latest available code. While this may be fine for a small project that does not demand utmost reliability/security, it won’t be fine for those that do. Those require some control over exactly which versions of dependencies are used — something that is not a built-in feature of Go.
Let’s clean up our workspace and start over, this time with dependency versioning in mind.
rm -rf $GOPATH/src
Step by Step Go Setup, With Glide
The sample app is set up to use Glide, a simple Go dependency versioning tool. Glide leverages a non-obvious Go feature: if a project contains a directory called vendor/
(more convention-over-configuration), then its contents are used for resolving dependencies over anything found in $GOPATH/src
. Glide reads a couple special files, glide.yaml
and glide.lock
, and puts all the appropriate dependencies at their correct versions in the vendor/
directory. Let’s get on it.
Step 1: Install Glide itself
That’s easy. Their site gives us a simple (but sketchy-looking) command to do it all at once:
$ curl https://glide.sh/get | sh
Step 2: Check out the code
This time, let’s set up the code manually as opposed to using go get
.
$ mkdir -p $GOPATH/src/github.com/fsufitch
$ cd $GOPATH/src/github.com/fsufitch
$ git clone https://github.com/fsufitch/sample-go-app.git
$ cd sample-go-app
Step 3: Use Glide to install our dependencies
Glide can be invoked in a few ways:
glide init
creates an initialglide.yaml
and gets us ready to use itglide get <x>
is likego get
; it installs a package, and in this case, also records it in theglide.yaml
andglide.lock
glide update
takes the sometimes non-specific version numbers inglide.yaml
and turns them into actual version hashes stored inglide.lock
; it also installs all the stuff it goes throughglide install
installs the specific package versions remembered by theglide.lock
file
In our case, we already have a proper glide.lock
so the last option is what we need.
$ glide install
[INFO] Downloading dependencies. Please wait...
[INFO] --> Fetching github.com/gorilla/context
[INFO] --> Fetching github.com/gorilla/mux
[INFO] Setting references.
[INFO] --> Setting version for github.com/gorilla/context to 08b5f424b9271eedf6f9f0ce86cb9396ed337a42.
[INFO] --> Setting version for github.com/gorilla/mux to 53c1911da2b537f792e7cafcb446b05ffe33b996.
[INFO] Exporting resolved dependencies...
[INFO] --> Exporting github.com/gorilla/context
[INFO] --> Exporting github.com/gorilla/mux
[INFO] Replacing existing vendor dependencies
Note: if your app needs to run without downloading absolutely anything other than its source code, you can safely add the
vendor/
directory to your version control. Those running on isolated cloud environments, take note!
Step 4: Install and run the app
Now that we have the vendor-ed dependencies in place, we can build and install our code with go install
, which skips the dependency-retrieval part of the build process. We can then run it just as we did before:
$ go install github.com/fsufitch/sample-go-app
$ sample-go-app $GOPATH/src/github.com/fsufitch/sample-go-app/static/
2018/01/16 22:30:51 Sending port and static path to server module (8080, /home/ubuntu/go-workspace/src/github.com/fsufitch/sample-go-app/static/)
2018/01/16 22:30:51 Creating router using static path: /home/ubuntu/go-workspace/src/github.com/fsufitch/sample-go-app/static/
2018/01/16 22:30:51 Creating uptime handler; unix now: 1516141851
2018/01/16 22:30:51 Serving on :8080...
That’s it! Really! You can even see the final result happily live here and here (unless it goes offline and I don’t bother fixing it).
Frequently Asked Questions
Can you have multiple Go workspaces?
Yes, and you can switch between them by manipulating GOPATH
. There are some neat environment management scripts that do just that. You can even have multiple values in your GOPATH
, with Go cascading through them, looking for your imports.
Why does your app need the path to the static files? They’re right there!
They’re “right there” at build time, but not at run time. Remember: Go creates a completely independent executable when it compiles a program. It is free to be placed anywhere, and has no knowledge of where its source files are (if they’re even on the same computer).
Also no, static files cannot be “baked in” to a Go binary. The Go compiler consumes one type of data: Go code. Thus, the only way to “include” a static file in your binary would be to convert it to Go. There are some ways to do that, but they are in most cases unnecessarily messy, in my opinion.
Why do I have to put my code several directories deep in Go’s weird structure instead of in my main code directory?
Convention. Over. Configuration. When code can be placed anywhere willy-nilly, it becomes more difficult to build. Unnecessarily difficult, according to the Go designers.
Why not just add a symbolic link to your code directory so you can find your code quickly?
I did <X> differently but it still works and I like it better, so your post is wrong.
That’s not a question. Also, there are other ways to do this for sure, but in my experience, this is the simplest and most idiomatic way I could find. Please do let me know if you have any feedback on how I could do better, though.
Why does Go not do something normal, like what C, Java, Python, or Malbolge do?
If Go were the popular one, then Python would seem not-normal instead. Go’s approach is just that: another approach. There is, of course, a more philosophical discussion surrounding Go being so “opinionated” in all areas of its design, but that is something that is better answered by its FAQ or the book written by the language’s designers.
That’s all I’ve got. Thanks for reading!
Thanks go to my co-workers, who are serving as the guinea pigs… er… gophers for this exercise in programming.