← Alle Beiträge

Go für Backend-Entwicklung — Warum wir darauf setzen

Matthias Bruns · · 7 Min. Lesezeit
go backend engineering

Die Qual der Wahl

Wenn Du ein neues Backend-Projekt startest, hast Du die Qual der Wahl: Java, C#, Python, Node.js, Rust, Kotlin, Go — jede Sprache hat ihre Community, ihre Frameworks und ihre Evangelisten. Jede hat Stärken. Jede hat Schwächen.

Wir bei Appetizer Labs haben uns für Go entschieden. Nicht weil es trendy ist (das war es vielleicht 2018). Nicht weil Google es entwickelt hat. Sondern weil es für das, was wir tun — Cloud-native Backend-Services bauen — schlicht die pragmatischste Wahl ist.

Hier ist warum.

Einfachheit ist ein Feature

Go hat 25 Keywords. Python hat 35. Java hat 67. Das klingt nach einer Nerd-Statistik, aber es hat reale Auswirkungen.

Go zwingt Dich zur Einfachheit. Es gibt keine Vererbung, keine Generics-Hölle (ja, es gibt jetzt Generics, aber bewusst eingeschränkt), keine Annotation-Magie, keine versteckten Kontrollflüsse. Code liest sich fast wie Pseudocode:

func GetUser(ctx context.Context, id string) (*User, error) {
    user, err := db.QueryUser(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("query user %s: %w", id, err)
    }
    if user == nil {
        return nil, ErrNotFound
    }
    return user, nil
}

Keine Magie. Kein Framework, das im Hintergrund Dependency Injection macht. Kein @Autowired, kein @Transactional, kein versteckter Proxy. Du siehst, was passiert. Jede Zeile.

Warum das wichtig ist: In einem Beratungskontext übergeben wir Code an Kundenteams. Einfacher Code ist Code, den andere verstehen können. Und Code, den andere verstehen, ist Code, den andere warten können. Das ist kein Nice-to-have — das ist der ganze Punkt.

Error Handling: Nervig, aber ehrlich

Ja, if err != nil schreibst Du hundertmal am Tag. Ja, das ist verbose. Aber weißt Du, was es nicht ist? Überraschend.

In Java fliegt irgendwo eine Exception, wird von irgendeinem Handler gefangen (oder auch nicht), und Du suchst eine halbe Stunde im Stack Trace. In Go ist jeder Fehler ein expliziter Wert, der explizit behandelt wird. Das ist nervig beim Schreiben und großartig beim Debuggen.

// Jeder Fehler ist sichtbar. Jeder Fehler wird behandelt.
file, err := os.Open(path)
if err != nil {
    return fmt.Errorf("open config %s: %w", path, err)
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
    return fmt.Errorf("read config %s: %w", path, err)
}

Kein try-catch mit 5 verschiedenen Exception-Typen. Kein throws in der Methodensignatur, das jeder ignoriert. Fehler sind Werte. Punkt.

Concurrency, die funktioniert

Go wurde für nebenläufige Systeme gebaut. Nicht nachträglich drangeschraubt — von Anfang an.

Goroutines sind der Kern davon. Eine Goroutine kostet ca. 2 KB Stack-Speicher beim Start. Ein Java-Thread kostet ca. 1 MB. Das heißt: Du kannst hunderttausende Goroutines gleichzeitig laufen lassen, ohne dass Dir der Speicher ausgeht.

func ProcessOrders(ctx context.Context, orders []Order) error {
    g, ctx := errgroup.WithContext(ctx)
    
    for _, order := range orders {
        order := order // capture loop variable
        g.Go(func() error {
            return processOrder(ctx, order)
        })
    }
    
    return g.Wait()
}

Das ist nicht nur syntaktisch elegant — es ist praxisrelevant. Ein typischer Backend-Service verbringt 90% seiner Zeit damit, auf I/O zu warten: Datenbank-Queries, HTTP-Calls, Dateisystem. Goroutines lassen Dich diese Wartezeiten parallelisieren, ohne Callback-Hölle, ohne Promise-Chains, ohne async/await-Viren, die sich durch Deine gesamte Codebasis fressen.

Channels: Kommunikation statt Shared State

// Producer-Consumer mit Channels: einfach und sicher
jobs := make(chan Job, 100)

// 5 Worker starten
for i := 0; i < 5; i++ {
    go func() {
        for job := range jobs {
            process(job)
        }
    }()
}

// Jobs einspeisen
for _, j := range allJobs {
    jobs <- j
}
close(jobs)

Keine Mutexes, keine Race Conditions, keine synchronized-Blocks. Channels sind Go’s Antwort auf “wie kommunizieren parallele Prozesse miteinander?” — und die Antwort ist verdammt gut.

Deployment: Eine Binary, Null Abhängigkeiten

Hier wird’s richtig praktisch. Ein Go-Programm kompiliert zu einer einzigen statischen Binary. Keine JVM, kein Node-Runtime, keine Python-Dependencies, kein pip install auf dem Server.

# Multi-stage Docker build für Go
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Das resultierende Docker-Image ist 10-20 MB groß. Kein Base-Image, keine OS-Packages, keine Sicherheitslücken in Bibliotheken, die Du gar nicht brauchst. Vergleich das mit einem typischen Java-Image (200-500 MB) oder einem Node.js-Image (150-300 MB).

In der Praxis bedeutet das:

  • Schnellere Container-Starts (wichtig für Autoscaling)
  • Kleinere Angriffsfläche (weniger im Container = weniger zu hacken)
  • Einfacheres Debugging (eine Binary, nicht ein Ökosystem)
  • Weniger Speicherverbrauch zur Laufzeit (kein GC-Overhead wie bei der JVM)

Ein typischer Go-HTTP-Service startet in unter 100 Millisekunden und verbraucht 20-50 MB RAM. Ein vergleichbarer Spring-Boot-Service braucht 5-15 Sekunden und 200-500 MB. Für Cloud-Native-Workloads, wo Du schnell hoch- und runterskalieren willst, ist das ein massiver Unterschied.

Die Standardbibliothek ist King

Go’s Standardbibliothek ist absurd gut. Für einen HTTP-Server brauchst Du kein Framework:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/users/{id}", getUser)
    mux.HandleFunc("POST /api/users", createUser)
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }
    
    log.Fatal(server.ListenAndServe())
}

Seit Go 1.22 kann der Standard-Mux sogar Pfad-Parameter und HTTP-Methoden routen. Für viele Services brauchst Du wirklich null externe Dependencies für das HTTP-Layer.

JSON-Handling, Crypto, Templates, Testing, Benchmarking, Profiling — alles in der Standardbibliothek. Das bedeutet weniger Abhängigkeiten, weniger Supply-Chain-Risiko und weniger node_modules-Ordner mit 500 MB.

Der Hiring-Faktor

Jetzt wird’s pragmatisch: Go-Entwickler zu finden ist einfacher als Du denkst. Und zwar aus einem Grund, der oft übersehen wird: Go ist einfach zu lernen.

Ein erfahrener Java- oder C#-Entwickler ist in 2-3 Wochen produktiv in Go. Nicht perfekt, aber produktiv. Weil die Sprache klein ist, die Konzepte klar sind und es nur einen Weg gibt, die meisten Dinge zu tun.

Vergleich das mit Rust (Lernkurve: Monate) oder dem Spring-Ökosystem (Lernkurve: Jahre, wenn Du alles verstehen willst).

Für den Mittelstand, wo Du nicht unbegrenzt Spezialisten einkaufen kannst, ist das entscheidend. Du brauchst keine Go-Experten — Du brauchst gute Entwickler, die Go in kurzer Zeit lernen.

Wo Go nicht die Antwort ist

Ehrlichkeit gehört dazu. Go ist nicht für alles die beste Wahl:

  • Datenintensive Anwendungen mit komplexen Abfragen: Wenn Dein Service hauptsächlich aus komplexen SQL-Queries besteht, sind ORMs wie Hibernate/JPA mächtiger als alles, was Go bietet.
  • Machine Learning: Python dominiert hier, und das hat gute Gründe. Go wird das nicht ändern.
  • Frontend-nahe Fullstack-Entwicklung: Wenn Dein Team auch Frontend macht, ist Node.js/TypeScript als durchgängiger Stack eine Überlegung wert.
  • Rapid Prototyping: Für einen schnellen Proof of Concept ist Python oder Ruby immer noch schneller am Start.

Unser Stack

Für die Neugierigen — so sieht unser typischer Go-Backend-Stack aus:

  • HTTP: Standard-Library + chi für Middleware
  • Datenbank: pgx für PostgreSQL, sqlc für typsichere Queries
  • Config: Environment Variables + envconfig
  • Logging: slog (Standard-Library seit Go 1.21)
  • Testing: Standard-Library + testcontainers-go für Integrationstests
  • Observability: OpenTelemetry

Kein Framework. Kein Spring Boot. Keine Magie. Nur Bibliotheken, die eine Sache gut machen und sich sauber zusammenstecken lassen.

Fazit

Go ist nicht perfekt. Keine Sprache ist perfekt. Aber für Cloud-native Backend-Services bietet Go eine Kombination, die schwer zu schlagen ist: Einfachheit, Performance, kleine Deployments und eine flache Lernkurve.

Für uns bei Appetizer Labs ist Go die Sprache, mit der wir am schnellsten robuste Services bauen, die unsere Kunden danach selbst warten können. Und das ist am Ende das, was zählt.

Nicht die eleganteste Sprache. Nicht die mächtigste. Sondern die pragmatischste.

Lesebarkeit

Schriftgröße