Cross Compiling Go apps

August 12, 2018

One of the nice things of Go is that you can cross compile for another architecture or operating system on your local system. If you want to build an linux version of your app, you don’t need to compile on an actual linux box, you can just do this on your mac.

As of Go 1.5, cross compilation is supported out of the box and doesn’t require any extra things to be installed. You can read all the details about this change here.

The cross compilation is configured by setting two environment variables, namely $GOOS and $GOARCH. These are special environment variables which influence the way the go tools work. They are discussed in great detail in the documentation.

The variable $GOOS indicates the operating system you are building for. The $GOARCH variable lets the compiler know for which architecture you want to build.

So, imagine we have a very basic go program which we want to compile:

package main

import "fmt"
import "runtime"

func main() {
    fmt.Println("OS: %s", runtime.GOOS)
    fmt.Println("Architecture: %s", runtime.GOARCH)
}

If you compile it without explicitely setting the environment variables, you will get a build for the current OS and architecture:

$ go build main.go
$ file main
main: Mach-O 64-bit executable x86_64

If we want to compile for a 64-bit Linux system, we would prepend the correct $GOOS and $GOARCH environment variables with the go build command:

$ GOOS=linux GOARCH=amd64 go build main.go
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

To do the same when you want to create a Windows executable, you will execute the following commands:

$ GOOS=windows GOARCH=amd64 go build main.go
$ file main.exe
main.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows

As you can see, when building for Windows, the .exe suffix is automatically appended.

There are many combinations possible which are all listed here.

The most common ones are:

$GOOS     $GOARCH
darwin    386      -- 32 bit MacOSX
darwin    amd64    -- 64 bit MacOSX
linux     386      -- 32 bit Linux
linux     amd64    -- 64 bit Linux
linux     arm      -- RISC Linux
windows   386      -- 32 bit Windows
windows   amd64    -- 64 bit Windows

Caveats

There are some small things which you need to take into account when using cross compilation as not everything is supported.

CGO is not supported

It is currently not possible to produce a cgo enabled binary when cross compiling from one operating system to another. This is that packages which use cgo invoke the C compiler as part of the build process to compile their C code and produce the C to Go mapping functions. In the current versions of Go, the name of the C compiler is hardcoded to gcc. This assumes the system default gcc compiler even if a cross compiler is installed.

Install vs build

When cross compiling, you should use go build, not go install. This is the one of the few cases where go build is preferable to go install.

The reason for this is that go install caches compiled packages, .a files, into the pkg/ directory which matches the root of the source code.

Take the following example. You are building $GOPATH/src/github.com/lib/mylib then the compiled package will be installed into $GOPATH/pkg/$GOOS_$GOARCH/github.com/lib/mylib.a.

The logic is the same for the standard library, which lives in /usr/local/go/src. They will be compiled to /usr/local/go/pkg/$GOOS_$GOARCH. This is a problem, because when cross compiling the go tool needs to rebuild the standard library for your target, but the binary distribution expects that /usr/local/go is not writeable.

Using go build rather than go install is the solution, because go build builds and then throws away most of the result (rather than caching it for later). This leaves you with the final binary in the current directory, which is most likely writeable by you.