Ein Go-Programm geht in wenigen Minuten von einer leeren Datei zum statisch gelinkten Binary. Dieser Artikel zeigt den vollen Workflow: Modul anlegen mit go mod init, eine main.go mit package main und func main() schreiben, das Programm mit go run direkt ausführen, mit go build ein Binary erzeugen und mit go install global verfügbar machen. Dazu kommt die Erweiterung um Argumente via os.Args, ein Blick auf func init() und die Cross-Compilation in einem einzigen Befehl. Du brauchst nur eine funktionierende Go-Installation (go version) und einen Editor.
Was wir bauen
Wir starten mit dem klassischen Hello World und erweitern es danach um eine zweite Funktion und einen einfachen Argument-Input. Das Ergebnis ist ein kleines CLI-Programm, das entweder einen Default-Gruß ausgibt oder einen übergebenen Namen begrüßt:
go run . Welt
go run . Michael
go run .Hallo, Welt!
Hallo, Michael!
Hallo, anonymer Gast!Auf dem Weg dahin gehen wir alle drei Standard-Build-Befehle einmal durch — go run, go build und go install — und schauen uns an, wie Go ein Programm initialisiert.
Projekt initialisieren
Jedes Go-Projekt ab Go 1.16 ist ein Modul. Ein Modul ist eine Sammlung von Paketen mit gemeinsamer Versionierung, beschrieben durch eine go.mod-Datei. Lege ein Verzeichnis an und initialisiere das Modul:
mkdir hello
cd hello
go mod init example.com/hellogo: creating new go.mod: module example.com/helloDie entstandene go.mod enthält drei Zeilen — den Modulpfad, die Go-Version und (später) Abhängigkeiten:
module example.com/hello
go 1.23Der Modulpfad ist gleichzeitig die Import-Adresse für andere Projekte. Ein Pfad wie example.com/hello ist ein guter Platzhalter für lokale Experimente; veröffentlichst du das Modul, sollte der Pfad zur tatsächlichen Repo-URL passen, etwa github.com/<user>/hello.
main.go schreiben
Lege jetzt eine Datei main.go neben go.mod an. Drei Pflicht-Bestandteile machen ein lauffähiges Go-Programm aus:
- Package-Klausel ganz oben — für ausführbare Programme
package main. - Imports für alles, was aus anderen Paketen kommt.
- Eine Funktion
mainohne Parameter und ohne Rückgabewert als Eintrittspunkt.
package main
import "fmt"
func main() {
fmt.Println("Hallo, Welt!")
}Hallo, Welt!Das war's. Sechs Zeilen, ein vollständiges Programm. Beachte, dass import "fmt" zwingend ist — Go kennt keine impliziten Imports, und unbenutzte Imports lehnt der Compiler aktiv ab.
go run — direkt ausführen
go run kompiliert das Programm in ein temporäres Verzeichnis und führt es sofort aus. Es gibt keine zurückgelassene Binary — ideal für schnelles Iterieren während der Entwicklung:
go run .Hallo, Welt!Du kannst statt . (alle .go-Dateien im aktuellen Paket) auch eine konkrete Datei angeben:
go run main.goSobald dein Programm aus mehreren Dateien besteht, ist go run . die robustere Variante — sonst beschwert sich der Compiler über fehlende Symbole aus den anderen Dateien.
go build — Binary erzeugen
go build produziert eine eigenständige, statisch gelinkte Binary im aktuellen Verzeichnis. Der Dateiname entspricht standardmäßig dem letzten Segment des Modulpfads (auf Windows mit .exe):
go build
ls -lh hello
./hello-rwxr-xr-x 1 user staff 2.0M May 4 12:00 hello
Hallo, Welt!Mit -o setzt du den Ausgabenamen, mit -ldflags="-s -w" lässt sich die Binary spürbar verkleinern (Debug-Info entfernen):
go build -o gruss
go build -ldflags="-s -w" -o gruss-slimDie Binary läuft ohne Go-Installation auf der Zielmaschine — kein Interpreter, keine Runtime-Pakete, nichts.
go install — Binary global verfügbar machen
go install baut die Binary und legt sie in den Bin-Ordner deiner Go-Installation. Diesen Pfad findest du mit go env GOBIN (falls gesetzt) oder als Default $(go env GOPATH)/bin:
go install
echo $(go env GOPATH)/bin
which hello/Users/michael/go/bin
/Users/michael/go/bin/helloDamit das Binary direkt aufrufbar ist, muss $(go env GOPATH)/bin in deinem PATH liegen. Das ist die bevorzugte Methode, um eigene CLI-Tools systemweit zu installieren — und auch, um fremde Tools per go install example.com/tool@latest einzuspielen.
| Befehl | Output | Anwendungsfall |
|---|---|---|
go run | nichts (temporär) | schnelles Ausprobieren während der Entwicklung |
go build | Binary im aktuellen Ordner | Release-Artefakt, CI-Output, Container-Image |
go install | Binary in $(go env GOPATH)/bin | eigene CLI-Tools systemweit nutzbar machen |
Erweiterung: Funktionen und Argumente
Erweitern wir das Programm: Eine eigene Funktion gruss baut den Begrüßungstext und gibt ihn — typisch Go — zusammen mit einem Fehlerwert zurück. Den Namen lesen wir aus den Kommandozeilen-Argumenten via os.Args:
package main
import (
"fmt"
"os"
"strings"
)
func gruss(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" {
return "Hallo, anonymer Gast!", nil
}
return fmt.Sprintf("Hallo, %s!", name), nil
}
func main() {
var name string
if len(os.Args) > 1 {
name = os.Args[1]
}
text, err := gruss(name)
if err != nil {
fmt.Fprintln(os.Stderr, "Fehler:", err)
os.Exit(1)
}
fmt.Println(text)
}Hallo, Welt!Drei Punkte, die hier sichtbar werden:
os.Argsist ein[]string. Das erste Element (os.Args[0]) ist der Programmname, ab Index1folgen die übergebenen Argumente.- Mehrfach-Returns —
grussgibt(string, error)zurück. Das ist der Standard-Weg, in Go Fehler zu signalisieren. fmt.Fprintln(os.Stderr, ...)+os.Exit(1)ist das Idiom für CLI-Fehler. Keinpanic, kein Exception-Throw.
Für ernsthafte CLIs willst du später flag (Stdlib) oder cobra statt os.Args direkt — der Mechanismus dahinter bleibt aber derselbe.
func init — was vor main läuft
Neben main gibt es eine zweite Sonderfunktion: init. Sie hat keinen Parameter, keinen Rückgabewert, und du rufst sie nie selbst auf. Go führt sie automatisch aus, nachdem alle Paket-Variablen initialisiert sind und bevor main startet.
package main
import "fmt"
var version = "dev"
func init() {
fmt.Println("init: setze Version")
version = "1.0.0"
}
func main() {
fmt.Println("main: Version =", version)
}init: setze Version
main: Version = 1.0.0Wichtig zum Init-Mechanismus:
- Eine Datei darf mehrere
init-Funktionen enthalten — sie laufen in Reihenfolge ihres Auftretens. - Auch andere Pakete dürfen
initdefinieren. Importierte Pakete werden vor dem importierenden Paket initialisiert. - Typische Use-Cases: Registrieren von Treibern (
database/sql,image), Validieren von Konfiguration, Vorberechnen von Lookup-Tabellen. - Verwende
initsparsam — versteckter Setup-Code ist schwerer zu testen als explizite Initialisierung inmain.
Cross-Compilation in einem Befehl
Eines der angenehmsten Features der Go-Toolchain: Du kannst auf macOS für Linux/ARM bauen, ohne Container, ohne Cross-Toolchain. Zwei Umgebungsvariablen genügen — GOOS (Zielsystem) und GOARCH (Zielarchitektur):
# Linux auf 64-Bit-ARM (z. B. AWS Graviton, Raspberry Pi 4)
GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64
# Linux auf 64-Bit-x86 (klassisches Server-Linux)
GOOS=linux GOARCH=amd64 go build -o hello-linux-amd64
# Windows auf 64-Bit-x86
GOOS=windows GOARCH=amd64 go build -o hello.exe
# macOS auf Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o hello-macDie unterstützten Kombinationen listet go tool dist list. Solange dein Code keine cgo-Abhängigkeiten hat, funktioniert Cross-Compilation aus dem Stand — und das Resultat ist eine statisch gelinkte Binary, die auf dem Zielsystem ohne weitere Installation läuft. Genau dieser Workflow hat Go zur ersten Wahl für Container-Workloads und Multi-Plattform-CLIs gemacht.
FAQ
Wieso braucht jede Datei package …?
Die Package-Klausel ist Pflicht — sie identifiziert, zu welchem Paket eine Datei gehört. Ohne package-Zeile lehnt der Compiler die Datei ab. Ausführbare Programme verwenden package main, Bibliotheken einen passenden Paketnamen (üblicherweise gleich dem Verzeichnisnamen).
Was ist der Unterschied zwischen go run und go build?
go run kompiliert in ein temporäres Verzeichnis und führt sofort aus — kein dauerhaftes Artefakt. go build legt eine Binary im aktuellen Ordner ab, die du verteilen, deployen oder in ein Container-Image kopieren kannst. Während der Entwicklung nimmst du go run, für Releases go build.
Wann brauche ich go.mod?
Praktisch immer. Seit Go 1.16 sind Module Default; ohne go.mod schlagen viele Befehle in modernem Go fehl. Der einzige Fall ohne go.mod ist ein wirklich isoliertes Single-File-Skript via go run main.go ohne externe Imports — und selbst dann ist go mod init der bessere Reflex.
Was passiert ohne func main() in einem Main-Package?
Build-Fehler: function main is undeclared in the main package. package main und func main() gehören zusammen — fehlt eines, baut nichts. Eine Bibliothek (kein package main) braucht dagegen kein main und lässt sich nur als Dependency importieren, nicht ausführen.
Wie nenne ich mein Modul (Module-Path)?
Für lokale Experimente reicht example.com/hello oder local/hello. Sobald du veröffentlichst, sollte der Modulpfad zur Repo-URL passen — z. B. github.com/<user>/<repo>. So kann Go das Modul direkt aus dem Internet auflösen, wenn jemand go install oder go get darauf ausführt.
Wieso wird import "fmt" benötigt — kann man das nicht weglassen?
Nein. Go hat keine eingebauten Print-Funktionen im Sprach-Namespace; Println lebt im Paket fmt. Ohne den Import bricht der Compiler ab. Umgekehrt sind unbenutzte Imports ebenfalls ein harter Fehler — der Compiler zwingt dich zu einem aufgeräumten Datei-Header.
Wozu init()?
Für Setup, der vor main laufen muss und nicht in eine reguläre Funktion passt: Treiber-Registrierung (database/sql-Driver, image/png-Decoder), Lookup-Tabellen vorberechnen, Konfiguration aus der Umgebung lesen. Verwende es zurückhaltend — viel init-Logik macht Tests und Mental-Modell schwerer.
Wie übergebe ich Argumente?
Weiterführende Ressourcen
Externe Quellen
- Go – Tutorial: Get Started
- Go – Tutorial: Create a Module
- Source file organization – Go Spec
- Program initialization – Go Spec
- Effective Go: A Tour of Go Programs