Installation with Go Language can be Simpler

I have been working on a hobby project written in Go for almost two years. It is a file manager called lf that runs inside the terminal. In this post, I would like to mention a few minor issues and some possibilities to simplify the installation of such command line tools.

Go does not always use static linking

Go normally uses static linking by default which is nice for command line applications. However, importing some packages seems to change this default to dynamic linking instead. A well known example is the net package which uses C library routines to resolve names when cgo is enabled. There is also a pure Go resolver implemented in the package which is used when cgo is disabled or the package is compiled for another platform. It is easy to override this behavior by disabling cgo using the CGO_ENABLED environmental variable:

env CGO_ENABLED=0 go build

Unfortunately, there is no way to specify this within the source code where I think this information really belongs. Many people use makefiles or shell scripts to iron out these details but then you can not simply use go get to install the package anymore. The best recommendation I get was to simply put the installation instruction with CGO_ENABLED as such:

env CGO_ENABLED=0 go get -u github.com/gokcehan/lf

This works more or less though it is a bit longer than it should be and it does not work in windows. It would be nice if there was a way to disable cgo using compiler pragma comments.

Go does not strip binaries by default

This is very similar to the previous issue. Go compiler includes debug symbols by default which increases binary sizes significantly. In order to avoid this, you can either pass -s and -w flags during compilation or strip the binary after compilation. Now the combined installation instruction looks as such:

env CGO_ENABLED=0 go get -u -ldflags="-s -w" github.com/gokcehan/lf

Compiler optimizations such as inlining and escape analysis are performed by default which can already make debugging tricky. Furthermore, race detection is disabled by default, so it is not possible to debug concurrency problems without a new build. With these in mind, the choice of including debug symbols by default seems arbitrary to me. Note that you still have your call stack to show up for runtime panics without debug symbols which is enough for most users. Hence, it would make more sense to not have debug symbols by default. At the very least, it would be nice if there was a way to exclude debug symbols somehow by setting ldflags using compiler pragma comments.

Go does not have versions in binaries

Long story short, the usual practice to have version numbers in the binary is to use an empty global varible for version in the code and pass the appropriate value during compilation. So a complete source install instruction should now look something like this:

go get -u github.com/gokcehan/lf
version=$(git -C $GOPATH/src/github.com/gokcehan/lf describe --tags)
env CGO_ENABLED=0 go get -u -ldflags="-s -w -X main.gVersion=$version" github.com/gokcehan/lf

If compiler pragma comments are to be added to set ldflags for the previous issue, this issue can be solved by adding a capability to run shell commands within these compiler pragmas.

GOOS/GOARCH and uname are different

POSIX systems have a standard uname system call and a corresponding command to get information about the current system. Among the information provided, there are kernel names with -s flag and machine hardware with -m flag that semantically corresponds to GOOS and GOARCH environmental variables respectively. Unfortunately, values used by uname are different from those used in Go. For example, on my machine, uname -s returns Linux but GOOS is set to linux. Similarly, uname -m returns x86_64 but GOARCH is set to amd64.

When binaries are cross built for various systems, natural tendency of most Go developers is to use the the values of GOOS and GOARCH variables in file names. Users then need to find the file from the list that is appropriate for their system. This process is sometimes automatized using a shell script that basically matches values returned from uname using a case statement. If same values were to be used for both uname and Go environmental variables, the corresponding file name would simply be something like:

lf-$(uname -s)-$(uname -m).tar.gz

This can eliminate the need for an installation script to have one-liner installation instructions instead. For example, a pre-built binary from a github release can be installed with a command like the following:

curl -L https://github.com/gokcehan/lf/releases/download/r1/lf-$(uname -s)-$(uname -m).tar.gz | tar xzC ~/.local/bin

Of course when you think about it, there is no actual issue here. You can always name your binaries using values returned by uname instead. The problem is that POSIX standard does not provide these names so one needs to hunt down these values. If values used by Go can not be changed at this point, maybe corresponding uname values can be given in the documentation or somewhere similar.

Github does not support download latest

Update: This is now available.

This bonus issue is not Go related but it is the last piece of the puzzle. In the above installation instruction, you need to be explicit about the release tag you want to download. The reason is that, even though github provides an url to view the latest tag, it does not have something similar for downloads. So the following url shows you the latest release in your project:

https://github.com/gokcehan/lf/releases/latest

But the following download url does not exists:

https://github.com/gokcehan/lf/releases/download/latest/lf-linux-amd64.tar.gz

If you want to download the latest release, you need to use the Github API to find out the latest release version first, and then you can download the file you want in a second request. If anyone from Github is reading this, this is something that would make lives easier.

Conclusions

I am generally quite happy with the build system in Go. Compilation is fast and cross compilation is a breeze. It is easy to setup a deployment system to have automatic cloud builds for all supported systems. Nowadays, I only need to push a git tag to have a new release. Things I mention here are just a few ideas which can make things even smoother.

In an ideal world, I would want to have two simple instructions for installation and upgrading. For a source install you may have the following:

go get -u github.com/gokcehan/lf

For a pre-built binary install, you may have the following:

curl -L https://github.com/gokcehan/lf/releases/download/latest/lf-$(uname -s)-$(uname -m).tar.gz | tar xzC ~/.local/bin

This may put an end to the endless curl-pipe-shell idiom discussion.