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:

bash terminal
go run . Welt
go run . Michael
go run .
Output
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:

bash terminal
mkdir hello
cd hello
go mod init example.com/hello
Output
go: creating new go.mod: module example.com/hello

Die entstandene go.mod enthält drei Zeilen — den Modulpfad, die Go-Version und (später) Abhängigkeiten:

Go go.mod
module example.com/hello

go 1.23

Der 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 main ohne Parameter und ohne Rückgabewert als Eintrittspunkt.
Go main.go
package main

import "fmt"

func main() {
    fmt.Println("Hallo, Welt!")
}
Output
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:

bash terminal
go run .
Output
Hallo, Welt!

Du kannst statt . (alle .go-Dateien im aktuellen Paket) auch eine konkrete Datei angeben:

bash terminal
go run main.go

Sobald 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):

bash terminal
go build
ls -lh hello
./hello
Output
-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):

bash terminal
go build -o gruss
go build -ldflags="-s -w" -o gruss-slim

Die 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:

bash terminal
go install
echo $(go env GOPATH)/bin
which hello
Output
/Users/michael/go/bin
/Users/michael/go/bin/hello

Damit 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.

BefehlOutputAnwendungsfall
go runnichts (temporär)schnelles Ausprobieren während der Entwicklung
go buildBinary im aktuellen OrdnerRelease-Artefakt, CI-Output, Container-Image
go installBinary in $(go env GOPATH)/bineigene 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:

Go main.go
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)
}
Output
Hallo, Welt!

Drei Punkte, die hier sichtbar werden:

  • os.Args ist ein []string. Das erste Element (os.Args[0]) ist der Programmname, ab Index 1 folgen die übergebenen Argumente.
  • Mehrfach-Returnsgruss gibt (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. Kein panic, 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.

Go main.go
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)
}
Output
init: setze Version
main: Version = 1.0.0

Wichtig zum Init-Mechanismus:

  • Eine Datei darf mehrere init-Funktionen enthalten — sie laufen in Reihenfolge ihres Auftretens.
  • Auch andere Pakete dürfen init definieren. 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 init sparsam — versteckter Setup-Code ist schwerer zu testen als explizite Initialisierung in main.

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):

bash terminal
# 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-mac

Die 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?

Für simple Fälle reicht os.Args — ein []string, dessen erstes Element der Programmname ist. Sobald du Flags, Defaults oder Sub-Commands brauchst, wechsel auf das Stdlib-Paket flag oder ein CLI-Framework wie cobra. Das Prinzip „Argumente sind ein String-Slice" bleibt aber identisch.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht