Dem Compiler lediglich das gewünschte Zielsystem mit auf den Weg geben, und schon purzelt am Ende ein passendes Programm heraus: Was für Entwickler unter C/C++ und vielen anderen Sprachen nach Wunschtraum klingt, ist für Go-Programmierer Realität.
Wer ein Programm für mehrere verschiedene Betriebssysteme und unterschiedliche Prozessoren bereitstellen möchte, greift in der Regel zu einem Cross-Compiler, der auf dem Rechner des Entwicklers ein Binary für ein komplett anderes Zielsystem erstellt. Bei GCC und vielen anderen bekannten Compilern gilt es jedoch, dazu erst mühsam eine passende Umgebung und die notwendigen Bibliotheken einzurichten.
Über diesen Aufwand schmunzeln Go-Programmierer nur: Bereits seit der Version 1.5 beherrscht Go von Haus aus Cross-Compiling [1]. Man gibt dabei lediglich die Zielarchitektur und das gewünschte Betriebssystem an, und der Compiler erstellt beim Aufruf automatisch ein passendes Binary. Auf diese Weise lässt sich beispielsweise unter Windows auf einem Intel-Prozessor schnell auch eine Fassung für den Raspberry Pi mit ARM-Prozessor und Linux generieren.
Da Go-Programme grundsätzlich statisch gelinkt sind, lässt sich die Programmdatei direkt auf das Zielsystem transferieren oder in einem Docker-Container starten. Diese Arbeitsweise vereinfacht ganz nebenbei noch das Deployment. Liegt der Go-Compiler auf der Festplatte, genügt der Aufruf aus der ersten Zeile von Listing 1, um aus dem Quellcode in der Datei »hello.go« ein 64-Bit-Programm für Windows zu generieren – egal, auf welchem System.
Listing 1
Cross-Compiler aufrufen
$ GOOS=windows GOARCH=amd64 go build -o hello hello.go
$ docker run --rm -v "$PWD":/var/src/hello -w /var/src/hello \
-e GOOS=windows -e GOARCH=amd64 golang go build -v hello.go
$ docker run --rm -it -v "$PWD":/var/src/hello -w /var/src/hello \
golang bash buildhello.sh
$ go install github.com/mitchellh/gox@latest
$ gox -os="windows" -arch="amd64"
Zwei Umgebungsvariablen lenken den Compiler auf das gewünschte Zielsystem: »GOOS« enthält das Betriebssystem, »GOARCH« die Architektur. Der Aufruf »go tool dist list« verrät die vom Go-Compiler unterstützten Betriebssysteme und Architekturen. Die dabei ausgespuckte Liste aus Abbildung 1 ist erstaunlich lang. Unter den zugelassenen Zielsystemen finden sich sogar AIX und Exoten wie Plan 9. Jede Zeile enthält eine weitgehend selbsterklärende Kombination aus Betriebssystem und Prozessorarchitektur. »linux/amd64« steht beispielsweise für ein Linux-System, das auf einem 64-Bit-Prozessor mit x86-Architektur läuft. Der Teil vor dem Schrägstrich gehört in die Umgebungsvariable »GOOS«, der Teil dahinter in »GOARCH«.
Was während des Übersetzens im Hintergrund passiert, zeigen die Parameter »-v« (Abbildung 2) und »-x« (Abbildung 3): Beim Bau für ein fremdes Zielsystem übersetzt der Go-Compiler zunächst alle Abhängigkeiten neu und legt sie in einem Cache ab, was den nächsten Durchlauf beschleunigt. Das Cross-Compiling von Go gelingt so gut, dass beispielsweise Owncloud das Verfahren bei seiner neuen Filesharing-Plattform Owncloud Infinite Scale (OCIS) einsetzt [2]. Dort baut der Go-Compiler Programme für Windows, MacOS und Linux; für die beiden letztgenannten Systeme existieren ARM-Binaries. Insgesamt stehen sieben verschiedene Zielplattformen bereit.

Abbildung 2: Unter Linux übersetzt der Go-Compiler zunächst alle Abhängigkeiten für ein 64-Bit-Windows.

Abbildung 3: Die Abhängigkeiten für eine Architektur landen im Cache, hier im versteckten Ordner ».cache/go-build/«.
In einem Go-Programm fragen Sie das Betriebssystem und die Architektur mithilfe des Moduls »runtime« über »runtime.GOOS« und »runtime.GOARCH« ab. Die entsprechenden Werte holt sich der Compiler nicht erst zur Laufzeit, sondern setzt sie bereits beim Übersetzen fest ein. Das Beispiel in Listing 2 gibt entsprechend die Architektur und das Betriebssystem aus.
Listing 2
OS und Architektur abfragen
package main
import "runtime"
import "fmt"
func main() {
fmt.Printf("Betriebssystem: %s Architektur: %s\n", runtime.GOOS, runtime.GOARCH)
}
Eingetuppert – im Docker-Container kompilieren
Der eigentliche Cross-Compiler-Lauf lässt sich in einen Docker-Container auslagern, den das Go-Projekt im Docker Hub bereitstellt [3]. Um mit ihm ein Binary für ein ganz bestimmtes Zielsystem zu bauen, geben Sie lediglich die beiden Umgebungsvariablen »GOOS« und »GOARCH« weiter. Bei installiertem Docker erzeugt der Befehl aus den Zeilen 2 und 3 von Listing 1 mit dem aktuellen Go-Compiler ein 64-Bit-Programm für Windows.
Die Direktive »run« startet einen Container, den Docker durch »–rm« nach dem Beenden des Go-Compilers automatisch wieder wegräumt. Der Quellcode liegt im Verzeichnis »/var/src/hello/«, das der Parameter »-v« in den Container durchreicht. »-w« bindet den Ordner zudem als Arbeitsverzeichnis ein, und »-e« setzt anschließend im Container die Umgebungsvariablen »GOOS« und »GOARCH«. Der Container im Docker Hub heißt »golang«. Benötigen Sie ein ganz bestimmtes Go-Release, hängen sie die entsprechende Versionsnummer an. So holt »golang:1.18« beispielsweise einen Container mit Go 1.18. Im hinteren Teil des Kommandos folgt der aufzurufende Compiler-Befehl.
Ein kleines Shell-Skript genügt, um Binaries für mehrere benötigte Zielsysteme zu erzeugen. Das Beispiel in Listing 3 nutzt dazu unter Linux die Fähigkeiten der Bash. Es erzeugt für Windows und Linux jeweils eine 64- und eine 32-Bit-Fassung, wobei es das Betriebssystem und die Architektur für die bessere Übersicht in den Programmnamen setzt. Dieses Skript lässt sich dem Docker-Container zuführen. Im Beispiel aus den Zeilen 4 und 5 von Listing 1 liegt es in der Datei »buildhello.sh«.
Listing 3
Binaries via Shell-Skript erzeugen
#!/bin/bash
for os in windows linux; do
for arch in amd64 386; do
GOOS=${os} GOARCH=${arch} go build -o hello_${os}_${arch} hello.go
done
done
In der Praxis sollten Sie jedoch statt eines Skripts ein passendes Build-System einspannen. Owncloud setzt dabei beispielsweise bei OCIS auf klassische Makefiles. Dank der beschriebenen Arbeitsweise von Go bleiben sie im Gegensatz zu manchem C/C++-Projekt kurz und übersichtlich, was sogar die Autotools überflüssig macht. Die Makefiles für OCIS finden Sie im entsprechenden Github-Repository und können Sie als Anregung für eigene Exemplare verwenden [4].
Praktische Hilfestellung durch GOX
Neben Skripts und etablierten Build-Systemen greift GOX Entwicklern unter die Arme [5]. Das in Go geschriebene Werkzeug erzeugt parallel mehrere Binaries für unterschiedliche Plattformen. Einige Linux-Distributionen halten es in ihren Repositories vor, alternativ beziehen Sie es über das Kommando aus der vorletzten Zeile von Listing 1.
GOX erzeugt parallel Programme für alle unterstützten Zielsysteme. Das gelingt allerdings nur, solange das Go-Modulsystem zum Einsatz kommt und die Konfigurationsdatei »go.mod« existiert. GOX hängt an den Programmnamen jeweils das Betriebssystem und die Architektur an. Die Windows-Version des Programms »hello« heißt somit »hello_windows_amd64.exe«. GOX orientiert sich zudem an der Anzahl der Prozessorkerne: Bei sechs Cores baut das Werkzeug ebenso viele Programme gleichzeitig. Auf Wunsch erzeugt das Tool nur ein Binary. Anders als beim Go-Compiler übergeben Sie Betriebssystem und Architektur in den Parametern »-arch« und »-os«. Im Fall des Windows-Binarys sieht das so aus wie in der letzten Zeile von Listing 1.
Fehlt einer der beiden Parameter, erzeugt GOX alle Binaries für das entsprechende Zielsystem. »gox -os=”linux”« baut folglich Linux-Programme für amd64, x386 und AMD.
So nützlich GOX auch ist, gibt es doch einen kleinen Haken: Das Werkzeug entwickelt sich derzeit nur schleppend weiter. Zudem sollten Sie darauf achten, die aktuellste Version zu nutzen. Anderenfalls versucht GOX, das Programm für Zielsysteme zu erstellen, die aktuelle Go-Versionen nicht mehr unterstützen, zum Beispiel eine 32-Bit-Fassung für MacOS. Dass GOX in der Praxis gute Dienste leistet, beweist wieder Owncloud: Dort erzeugt das Tool zuverlässig die verschiedenen Fassungen von OCIS.
Die fertigen Binaries müssen nun den Weg auf die Zielmaschinen finden. Im einfachsten Fall offeriert ein Download-Server die erzeugten Programme. Die Zielmaschinen holen dann beispielsweise über das Tool Wget die jeweils aktuelle Fassung automatisch von dort ab. OwnCloud stellt seine OCIS-Programme [6] auf Github sowie auf seinen eigenen Servern [7] bereit. Auf Letzteren landen auch Entwicklerversionen [8].
Alternativ oder ergänzend schieben Sie die vom Go-Compiler erzeugten Programme in Docker-Container. Da in der Regel das einzelne Binary für den Betrieb ausreicht, drängt sich das geradezu auf. Abschließend lässt sich eine beliebige Build-Pipeline oder ein CI/CD-System integrieren. Bei Owncloud kommt dafür DRONE [9] zum Einsatz, das sowohl die Binaries als auch die passenden Docker-Images erzeugt.
Fazit: Ganz grenzenlos geht es doch nicht
Die schöne, bequeme Go-Welt hat allerdings zwei Grenzen. Zum einen stehen einige Module nicht für alle Plattformen bereit. Das bekommen Sie als Entwickler vor allem dann zu spüren, wenn Sie Module von Drittherstellern einbinden. Da diese Limitierungen durchweg dokumentiert sind, können Sie die Plattformunabhängigkeit schon bei der Auswahl der Module berücksichtigen und sich später beim Übersetzen darauf einstellen.
Zum anderen ermöglicht der Mechanismus CGO Go-Programmen, C-Code aufzurufen, den ein externer Compiler übersetzt. Damit das Cross-Compiling weiter klappt, muss der C-Compiler Binaries für die Zielplattformen erstellen. Sofern diese Voraussetzungen erfüllt sind, lässt sich beim Aufrufen des Go-Compilers der zur Zielplattform passende Aufruf des C-Compilers angeben. Das Beispiel aus Listing 4 schaltet erst via »CGO_ENABLED=CGO« CGO ein und wählt dann »android-armeabi-gcc« als C-Compiler. Der Rest funktioniert wie gewohnt, wobei der Befehl ein Binary für Android erstellt.
Listing 4
Cross-Compiling mit CGO
$ CGO_ENABLED=1 CC=android-armeabi-gcc GOOS=android GOARCH=arm GOARM=7 go build -o hello_android hello.go
Das erstaunlich einfache Cross-Compiling liefert ein weiteres Argument für den Umstieg auf Go. Dass man nicht für jede Zielplattform eine passende Compiler-Umgebung braucht, vereinfacht die Entwicklung und das Deployment drastisch. Der Cross-Compiler von Go arbeitet zuverlässig, wie der Einsatz bei Owncloud zeigt. Wer mit Go arbeitet, sollte daher Nutzer anderer Plattformen berücksichtigen und entsprechende Binaries erzeugen – das kostet jeweils nur einen weiteren Compiler-Aufruf. Neben dem Cross-Compiling besitzt Go noch einige andere interessante Eigenschaften, die sogar den klassischen LAMP-Stack ersetzen – allerdings mit Folgen für die Entwickler. (csi)
Infos
- Cross-Compiling in Go: https://go.dev
- Infinite Scale: https://owncloud.com/infinite-scale/
- Description: https://hub.docker.com/_/golang
- Makefiles für OCIS: https://github.com/owncloud/ocis/tree/master/.make
- GOX: https://github.com/mitchellh/gox
- OCIS-Versionshistorie: https://github.com/owncloud/ocis/releases
- OCIS 1.18.0: https://download.owncloud.com/ocis/ocis/1.18.0
- OCIS-Entwicklerversionen: https://download.owncloud.com/ocis/ocis/testing/
- DRONE: https://owncloud.dev/ocis/development/continuous-integration/







