In part 5 of Goalng Pros & Cons for DevOps, we discuss cross-platform compilation for Go projects.
Be sure to read up on the last post about “The Time Package and Method Overloading” if you missed it, or subscribe to our blog updates to be notified when the rest of the series is published.
As someone who primarily uses linux, it can be a real pain to deal with Windows on the rare occasions that I have to. This is especially true when working on our Smart Agent™, which runs on both Linux and Windows and dives deep into OS-specific issues for both our log management and monitoring products.
Since our agent is written in Golang, though, actually compiling the code to run on Windows is fairly painless, and can easily be done in a Linux environment. The bulk of the work is handled by passing in two variables when running the go build
command: GOARCH and GOOS.
A full list of the possible combinations is available by running go tool dist list
, and on Go 1.8 there are 38 combinations. The below example shows how to build Linux and Windows for both AMD64 and Intel i386 architectures, and you can easily see how a Makefile could be created to easily build for many different targets.
GOOS=linux GOARCH=amd64 go build -o bin/myapp_linux_amd64 myapp
GOOS=windows GOARCH=amd64 go build -o bin/myapp_windows_amd64 myapp
GOOS=linux GOARCH=386 go build -o bin/myapp_linux_386 myapp
If your project uses cgo, you will have a little more trouble. In order to get cgo code to compile cross-platform, you have to have the correct toolchain set up on the build machine. It had been a while since I had to deal directly with gcc, but it was relatively easy to find the correct commands to set up an Ubuntu 16.04 box. Below are some one-liners you can use to set up your build machine when figuring things out with cgo:
# Install cgo dependencies
apt-get install -y gcc libsystemd-dev gcc-multilib
# cgo for linux/386
apt-get install -y libc6-dev-i386
# cgo for windows
apt-get install -y gcc-mingw-w64
And the modified build commands would be:
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=gcc go build -o bin/myapp_linux_amd64 myapp
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build -o bin/myapp_windows_amd64 myapp
CGO_ENABLED=1 GOOS=linux GOARCH=386 CC=gcc go build -o bin/myapp_linux_386 myapp
The golang documentation site is really well done. It has useful examples, links to source code, and of course there is the playground for testing out snippets of code. One shortcoming of the golang docs, however, is when you're using code that behaves differently on Windows.
Some pages, like the os page do a pretty good job about explaining the difference between Unix and WIndows systems for many functions. I wonder, though, why they even include some functions like Getegid in the standard libraries for windows with comments like:
Getegid returns the numeric effective group id of the caller.
On Windows, it returns -1.
I would much rather the compiler fail when using functions like this in code built for Windows, instead of finding out only at runtime that the value is useless, or the function call does nothing.
Other pages in the documentation cop out completely with a blanket statement about Windows, such as the exec page:
Note that the examples in this package assume a Unix system. They may not run on Windows, and they do not run in the Go Playground used by golang.org and godoc.org.
Very helpful.
This con is only really apparent because of how easy it is to "try out" compiling for windows after developing a feature on Unix. There has been quite a few times where I will use an open source library to write a feature for linux, and accidentally break the Windows build because the library uses Unix-only calls. There are two relatively simple solutions:
If you have the time and feel like contributing back to the project, make a PR that adds in Windows support! Beware that this can be very time-consuming depending on your dev setup, because even though building for Windows is easy, testing is not.
Golang makes it super easy to exclude or include files for a build using build constraints. For example, to only include a dependency (that only works in Windows) in a file that is built only for NT you could do:
// +build windows
package mypackage
import "github.com/bluematador/windows-only"
You can also negate which GOOS to build on:
// +build !windows
package mypackage
import "github.com/bluematador/linux-only"
The nice thing here is it forces you to organize things a little bit more, and you can still have the same function callable in both OSes but with different behavior by using build constraints on the file it is defined in.
Every other week or so, we are posting a new guide like this in our six-part series on “Golang Pros & Cons for DevOps.” Next up: #6: Defer Statements and Package Dependency Versioning.