GoLang: Cross Compiling for Linux and Windows platforms

A nice feature of the Go language is the ability to build binaries for multiple platforms directly from a single source system.  As an example, even from a development Windows 7 32-bit machine, you can build binaries for both 64 bit Linux and Windows 2012 Servers.

Before Go 1.5, you needed a compiler for the target architecture, but now that the entire tool chain is written in Go, building for multiple architectures is easy.

And unlike other languages where additional external libraries need to be copied or downloaded on the target system, Go dependencies are generally statically linked [1,2,3,4] into a single binary which makes portability that much easier.

Building for default architecture

Let’s use a simple go file as an example.  Assuming you have installed Go and have the proper environment variable setup, run the following commands on Ubuntu (or the equivalent on Windows):

$ cd $GOPATH
$ mkdir -p src/myarch
$ cd src/myarch

And then either copy the simple go file below, or download it directly from my github project.

// put at $GOPATH/src/myarch/myarch.go
package main

import "fmt"
import "runtime"

func main() {
    fmt.Printf("Hello from: %s %s\n",runtime.GOOS,runtime.GOARCH)

Being on a 64 bit Ubuntu 14.04 host, doing either a default build or specifying a 64 bit binary explicitly results in:

$ go build
$ ./myarch
Hello from: linux amd64

$ env GOARCH=amd64 go build
$ ./myarch
Hello from linux amd64

And you can also verify the binary target architecture by having the ‘file’ command look at the header:

$ file ./myarch

./myarch: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), statically linked, not stripped

For Linux, to check if the binary is statically or dynamically linked, use the ‘ldd’ and ‘readelf’ utilities.  The below output is for a statically linked binary.

$ ldd ./myarch
not a dynamic executable

# readelf should return empty if statically linked
$ readelf -l myarch | grep interpret

Alternatively, a dynamically linked binary would have return back results that look similar to:

$ldd ./mybinary
 linux-vdso.so.1 => (0x00007ffea676e000)
 libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd8acfee000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd8acc26000)
 /lib64/ld-linux-x86-64.so.2 (0x00007fd8ad20c000)

$ readelf -l mybinary | grep interpret
 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

To force a Go binary to be statically linked set the CGO_ENABLED environment variable to 0 before running the build:

$ export CGO_ENABLED=0
$ go build -a

Building for Linux 32 bit

If I needed to build a 32 bit Linux binary (even though my host server is a 64 bit Linux), I could specify a different target architecture.

$ env GOARCH=386 go build
$ ./myarch
Hello from: linux 386

And you can also verify the binary by having the ‘file’ command look at the header:

$ file ./myarch

./myarch: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

Building for Windows 32 bit

If I needed to build a 32 bit Windows binary (even though my host server is a 64 bit Linux), I could specify a different target architecture and OS.

$ env GOOS=windows GOARCH=386 go build

And you can also verify the binary by having the ‘file’ command look at the header:

$ file ./myarch

myarch.exe: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

If you copy this file over to a Windows 32-bit host and run it:

> myarch.exe
hello from: windows 386


And if you try to run a 64 bit binary on a 32 bit Windows machine, you get an error saying that the exe is “is not compatible with the version of Windows you’re running”, as expected.


















go build -a -tags netgo (for further hint on static linking, but CGO_ENABLED=0 has been enough for me)