Cross compiling in Go

    Despite the fact that cross-platform became virtually a standard attribute of almost all modern languages ​​and libraries, creating a truly cross-platform product was still not easy. Compiled languages ​​and related libraries required complex installation and configuration of the build environment and libraries, and interpreted languages ​​required the presence or deployment of the necessary version of the interpreter. There are many projects trying to make this process a little simpler, but often the only solution was to install a separate server and compile natively.

    In Go, cross-platforming has reached a level where for the first time you can safely abandon compile farms, specially configured dev environments, virtual machines for assembly, or chroot / docker-dev solutions. And this is another serious game-changer, about which I want to talk more and show with examples
    Let's go.

    As you know, Go deliberately refused dynamic linking - for a number of reasons, the main of which is very similar to the usual explanation of the design of almost any aspect of Go - “the advantages of [dynamic linking] are much less than its shortcomings and the complexity that it brings to the architecture”. Well, the main reason for the emergence of dynamic linking was the desire to save resources - primarily disk space and memory - which are now quite cheap, not only on servers, but also in embedded devices (copters, for example, carry 1-2 GB RAM!). In general, listing the pros and cons of a separate linking method will pull you to a separate post, so for now we just accept it as it is - in Go we always have a static binary at the output.

    At the moment, for the current version of Go 1.4.1, the following platforms are supported:
    • Linux 2.6 1 and later - amd64, 386, arm
    • MacOS X 10.6 and later - amd64,386
    • Windows XP and above - amd64, 386
    • FreeBSD 8 and higher - amd64, 386, arm
    • NetBSD - amd64, 386, arm
    • OpenBSD - amd64, 386
    • DragonFly BSD - amd64, 386
    • Plan 9 - amd64, 386
    • Google Native Client - amd64p32, 386
    • Android - arm

    1 - kernels 2.6.23 and higher are officially supported, but in reality everything works on earlier kernels of branch 2.6 - for example, many people use Go on RHEL5 / CentOS5 from 2.6.18.

    Go 1.5 is expected to support iOS.
    It is also noteworthy that initially there was no Windows support in Go - the team is small, andthere was no one to get your hands on code implementation for Windows, but thanks to the fact that the project was opened for open-source development - the Windows port was written very quickly by third-party people and integrated into the official code base.

    Although the processes described below will be absolutely identical for all platforms (with the exception of Android and Native Client (NaCl), which require extra body movements), in the rest of this article we will assume by default that you are using one of the three most popular desktop platforms - Linux, MacOS X or Windows. In addition, for greater simplicity, I will mean that we write and use exclusively Go code, without the need to link to C libraries (and, therefore, without the need to use cgo / gcc). There is still a separate case - when you need to use a number of functions from the standard library tied to cgo, but I will write about this in a separate chapter at the end.

    Toolchain preparation

    The first step that needs to be done is to build a toolchain for the desired platform.

    We go to the directory with the source code Go (it’s also $ GOROOT / src, you always have it on your machine) and rebuild it for the desired platform, say Windows / amd64:
    cd $(go env GOROOT)/src
    sudo GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./make.bash --no-clean

    The process takes about 26 seconds on a Macbook Air 2012. The make.bash script is a standard Go build script that you would install Go if you were installing from source. It collects, in fact, Go, and the entire standard library, only this time - for the windows / amd64 platform.
    Also, for the reason mentioned above, we have disabled CGO support.

    GOOS and GOARCH Values

    The table of values ​​GOOS (if anyone knows how to make a table at 50% width on Habré, tell me):
    OS$ GOOS
    MacOS xdarwin
    DragonFly BSDdragonfly
    Plan 9plan9
    Native clientnacl

    And GOARCH:
    Architecture$ GOARCH
    AMD64 with 32 pointersamd64p32

    Example 1. A web server written and compiled on Linux for Windows

    We will write a simple web server, which is easier to write in Go than in some languages ​​/ libraries parsing the command line.
    package main
    import (
    func Handler(w http.ResponseWriter, r *http.Request) {
    	w.Write([]byte("Hello, world\n"))
    func main() {
    	http.HandleFunc("/", Handler)
    	log.Println("Starting HTTP server on :1234")
    	log.Fatal(http.ListenAndServe(":1234", nil))

    And put it together for Windows 32- and 64-bit:
    GOOS=windows GOARCH=386 go build -o http_example.exe
    GOOS=windows GOARCH=amd64 go build -o http_example64.exe

    We check:
    $ file http_example*.exe
    http_example.exe:   PE32 executable for MS Windows (console) Intel 80386 32-bit
    http_example64.exe: PE32+ executable for MS Windows (console) Mono/.Net assembly

    I think it’s not necessary to say that both binaries are ready for copying to the target Windows system and will work.

    Example 2. Cross-compilation under ARM for Nokia N9 phone

    I must say right now that I don’t work closely with embedded devices, so I may not know any details - so I’ll try not to delve into this topic, but in general I’m following the situation with Go on embedded. Generally speaking, Go was not positioned as a language for embedded platforms, which, however, did not prevent people from actively starting to use it in this area. Perhaps the reason is that the embedded industry has made a leap forward, and now the “embedded” device no longer means a critically small amount of resources, and perhaps the trade-offs not in favor of saving memory in Go turned out to be much less tangible in practice, but there is a fact - Go has already created a lot of projects like Gobot (a robotics framework for a whole bunch of platforms - from Arduino, Raspberry PI and Beaglebone Back to LeapMotion, Pebble and ArDrone) orEMBD (a framework for working with hobby boards), and PayPal has been using Go in its beacon device for wireless checks and payments for a couple of years now .

    For example, take the Nokia N9 (or N950, who is lucky) - and collect the above example for it:
    GOOS=linux GOARCH=arm go build -o http_example_arm
    scp http_example_arm developer@

    So simple, yes.

    For ARM platforms, in fact, you may still need to specify the GOARM flag, but here, if the default version does not fit, the binary on the target platform will give a clear message, like this:
    runtime: this CPU has no floating point hardware, so it cannot 
    run this GOARM=7 binary. Recompile using GOARM=5.

    Automate the process

    It would seem that it might be easier to specify a single variable before go build. But there are situations when the code needs to be collected and deployed on different platforms 100 times a day. For such tasks, there are several projects for automating the processes of preparing toolchains and, directly, assembling code for the desired platform.


    Installation and preparation of all possible toolchains at once:
    go get
    gox -build-toolchain

    Now, instead of “go build”, we write “gox”:
    $ gox
    Number of parallel builds: 4
    -->      darwin/386:
    -->    darwin/amd64:
    -->       linux/386:
    -->     linux/amd64:
    -->       linux/arm:
    -->     freebsd/386:
    -->   freebsd/amd64:
    -->     openbsd/386:
    -->   openbsd/amd64:
    -->     windows/386:
    -->   windows/amd64:
    -->     freebsd/arm:
    -->      netbsd/386:
    -->    netbsd/amd64:
    -->      netbsd/arm:
    -->       plan9/386:

    You can specify a specific package or a specific platform:
    gox -os="linux"
    gox -osarch="linux/amd64"

    The remaining command line arguments are identical to go build. Intuitive enough.


    GoCX is one of the most famous wrappers around cross-compilation features, but with an emphasis on packaging (it can do .deb even) and various buns for automated assemblies. I didn’t use it myself, so for those who are interested, see the site and the documentation.

    Understanding CGO

    If someone watched a video from the GopherCon 2014 conference last spring in Denver, he probably remembers Alan Shreve's “Build Your Developer Tools in Go” talk, and one of the things he says quite categorically: “don't use cross compilation, compile natively. " Next is the explanation - the reason is Cgo. If you do not need to use cgo, then everything is ok. And in fact, a very small part of the very specific Go code needs third-party C libraries. What is the problem?

    The problem is that some functions of the standard library are dependent on cgo. That is, if we collect Go with CGO_ENABLED = 0, they simply will not be available and at the compilation stage we will get an error. Despite the fact that there is a very convenient and beautiful workaround, let's see what exactly in the standard library depends on cgo.

    Fortunately, this is easy to do:
    # cd $(go env GOROOT)/src/
    # grep  -re "^// +build.*[^\!]cgo" *
    crypto/x509/root_cgo_darwin.go:// +build cgo
    net/cgo_android.go:// +build cgo,!netgo
    net/cgo_linux.go:// +build !android,cgo,!netgo
    net/cgo_netbsd.go:// +build cgo,!netgo
    net/cgo_openbsd.go:// +build cgo,!netgo
    net/cgo_unix_test.go:// +build cgo,!netgo
    os/user/lookup_unix.go:// +build cgo
    runtime/crash_cgo_test.go:// +build cgo

    Briefly go through these files:
    • crypto / x509 / root_cgo_darwin.go - implements one function for obtaining root X.509 certificates in MacOS X. If you do not use this feature explicitly - it's okay, without cgo everything will work for you.
    • net / cgo_android / linux / netbsd / openbsd / cgo_unix_test.go - the code needed to use the native DNS resolver in different unix. Details below.
    • os / user / lookup_unix.go - functions from the os / user package - to get information about the current user (uid, gid, username). Used by getpwuid_r () to read passwd entries
    • runtime / crash_cgo_test.go - file with tests for handling hooks, nothing relevant

    Now more about DNS-resolver.
    Each file from the list (which is compiled only for its platform thanks to the // + build tags) contains the implementation of the single function cgoAddrInfoFlags (), which, in turn, is used in cgoLookupIP (), which is used in dnsclient_unix.go, in which we we find the goLookupIP () function, which serves as a fallback option in the absence of cgo-enabled code, and then we find an explanation:
    // goLookupIP is the native Go implementation of LookupIP.
    // Used only if cgoLookupIP refuses to handle the request
    // (that is, only if cgoLookupIP is the stub in cgo_stub.go).
    // Normally we let cgo use the C library resolver instead of
    // depending on our lookup code, so that Go and C get the same
    // answers.

    goLookupIP actually resolves only on the Hosts file and on the DNS protocol, which for most systems is approx. But there may be problems if non-standard methods of resolving names are used in the system. I believe that in 99% of cases, hosts and dns will be more than enough.

    The bottom line is - if your code does not use C / C ++ - libraries through Cgo, and does not use the following two things:
    • validation of x.509 certificates, which should work on MacOS X
    • guaranteed to receive system information about the current user

    then all the troubles with Cgo can be scored .

    The first part (from X.509) is actually not so rare. If I understand correctly - this code is needed if your program uses standard net / http.StartAndListenTLS () - and you use real certificates that really need to be verified.

    Therefore, in brief about a simple workaround around this topic - it is called gonative , and does one simple thing - it downloads from the official website binary versions of golang of the desired version for the desired platform, in which there are already compiled binaries of all standard packages and, in fact, completes the process of “compiling toolchain with cgo code. "
    All you need to do is install it (go get and execute one simple command:


    And then use the standard cross-compilation procedures, as before, with pens or through gox / gocx.
    Read more about gonative here:

    Practical use

    Now about the main thing - putting it into practice. I have used only three schemes in production so far - “build on darwin / amd64 -> deploy on linux / 386”, “linux / amd64 -> linux / 386” and “linux / amd64 -> windows / amd64”. These are products that have been working fully for more than a year. The third case (deployment on windows) then generally took me by surprise - there was a server running successfully on Linux, and then suddenly it suddenly became necessary to run it on Windows. Moreover, "urgently needed." Remembering the sleepless nights of experience with cross - yes, what’s cross there, just with Qt compilation for deployment on Windows - the 60-second process “google how to do this → build toolchain → recompile the project → deploy on windows” - was just a shock, I even I could not believe my eyes.

    But here the next moment arises - since cross-compilation and deployment become so simple and fast, there is an incentive to add all the dependencies on files — be it configs, certificates or anything else — to embed them in the binary too. However, this is a fairly simple task, even for third-party libraries, thanks to the efficient use of the io.Reader interface and the go-bindata package , but this is a topic for a separate article.

    I hope I haven’t missed anything of the main.
    But overall, this is actually a very significant difference with all previous cross-build experience. To be honest, I'm still not used to this change. Virtual machines with a configured dev environment are no longer needed, docker images are not needed for assembly - in general, dev-environment disappears as such. This is too sharp a game changer to get used to so quickly.

    References golang-app-for-windows-from-linux.html

    Also popular now: