3. Februar 2026
8 mins

Golang in einer PHP-Welt: Wie ich neue hochperformante Services integriere ohne alles umzubauen

Golang in einer PHP-Welt – Artikelbild

Die Frage die sich irgendwann stellt

PHP läuft. Das Produkt wächst. Und irgendwann läuft ein bestimmter Service nicht mehr mit.

Nicht das gesamte System – ein konkreter Engpass. Eine API die unter Last einbricht. Ein Service der bei hoher Concurrency ins Schwitzen kommt. Ein Teil der Architektur der deutlich macht dass PHP für genau diesen Use Case nicht mehr die richtige Antwort ist.

Die naheliegende Reaktion ist ein Rewrite. Ich habe erlebt wie das endet. Die pragmatische Antwort ist eine andere: den Engpass gezielt ersetzen ohne den Rest anzufassen. Golang für die Teile die Performance brauchen. PHP für alles was bereits läuft und keinen Grund hat sich zu ändern.

Die Kommunikation zwischen PHP und Golang

Die erste Architekturentscheidung war die wichtigste: wie kommunizieren PHP-Services und Golang-Services miteinander?

REST war die offensichtliche Antwort. Ich habe mich dagegen entschieden – für alles was im Backend zwischen Services kommuniziert. gRPC über Protobuf ist intern der Standard.

Die Gründe sind praktisch. Protobuf erzwingt explizite Verträge. Änderungen am Interface sind sofort sichtbar als Schema-Diff statt als implizite Verhaltensänderung die niemand dokumentiert hat. Und Performance die bei hohem Durchsatz messbar einen Unterschied macht.

Ein vereinfachter Protobuf-Contract für einen Auth-Service sieht so aus:

syntax = "proto3";

package auth.v1;

option go_package = "github.com/acme/auth/gen;auth";

service AuthService {
  rpc ValidateToken (ValidateTokenRequest) returns (ValidateTokenResponse);
}

message ValidateTokenRequest {
  string token = 1;
}

message ValidateTokenResponse {
  string user_id        = 1;
  repeated string roles = 2;
  bool   valid          = 3;
}

Aus dieser Definition wird automatisch sowohl der Golang-Server-Code als auch der PHP-Client-Code generiert. PHP kommuniziert mit dem Golang-Service ohne dass jemand manuell Serialisierung schreiben muss.

Der PHP-Client sieht dann so aus:

$client = new AuthServiceClient('auth-service:50051', [
    'credentials' => Grpc\ChannelCredentials::createInsecure(),
]);

$request = new ValidateTokenRequest();
$request->setToken($token);

[$response, $status] = $client->ValidateToken($request)->wait();

if ($status->code !== Grpc\STATUS_OK || !$response->getValid()) {
    throw new UnauthorizedException();
}

$userId = $response->getUserId();

REST bleibt trotzdem im Stack – aber mit klar definierten Grenzen. Health-Endpunkte laufen über REST weil Monitoring-Tools und Load Balancer HTTP sprechen. Alles was direkt customer-facing ist ebenfalls – weil Browser und mobile Clients kein gRPC sprechen. Das Schöne dabei: aus einer Protobuf-Definition lässt sich automatisch ein REST-Gateway generieren über den gRPC-Gateway-Proxy. Ich schreibe den Contract einmal und bekomme beide Protokolle ohne doppelte Implementierung.

Weitere Informationen dazu in der offiziellen Dokumentation: grpc-ecosystem/grpc-gateway

Wo ich angefangen habe

Der erste Golang-Service war Authentifizierung. Nicht wegen Performance – Auth war nicht der Engpass. Sondern weil Auth strukturell ideal für einen ersten Schnitt ist. Klar abgegrenzt, definierter Input und Output, wenige externe Abhängigkeiten. Das Team lernt Golang in Produktion ohne dass ein Fehler das gesamte System gefährdet.

Danach kam das Payment Gateway. Das war der eigentliche Performance-Treiber. Hohe Last, strikte Latenzanforderungen, komplexe Integration mit externen Payment-Providern. PHP hatte hier seine Grenzen erreicht. Golang nicht.

Die Reihenfolge war kein Zufall. Mit Auth hatte das Team Golang in Produktion gelernt bevor es an den kritischen Service ging.

Das eigentliche Problem: Das mentale Modell

Die größte Herausforderung war nicht technisch. Sie war im Kopf.

PHP-Entwickler denken in Request-Response-Zyklen. Ein Request kommt rein, wird verarbeitet, eine Antwort geht raus. Der Prozess gehört für die Dauer des Requests diesem einen Request. Das ist so tief verankert dass es unbewusst wird – auch wenn PHP theoretisch Möglichkeiten bietet außerhalb dieses Modells zu arbeiten.

Golang denkt fundamental anders. Das Concurrency-Modell mit Goroutines und Channels ist kein Feature das man bei Bedarf einsetzt – es ist die Art wie Golang unter Last funktioniert. Wer das ignoriert baut Services die sich wie PHP anfühlen aber nicht wie Golang performen.

Genau das ist passiert. Die ersten Golang-Implementierungen im Team verarbeiteten Anfragen sequentiell. Nicht weil jemand einen Fehler gemacht hat – sondern weil niemand bewusst in Concurrency gedacht hatte. Der Code war korrekt. Er war nur nicht concurrent. Unter Last wurde das sofort sichtbar: der Service der PHP ablösen sollte war langsamer als sein Vorgänger.

Der Fix war technisch trivial. Das Verständnis dahinter nicht. Ein einfaches Beispiel das den Unterschied zeigt:

// Sequentiell – PHP-Denken in Golang
func processRequests(requests []Request) []Result {
    results := make([]Result, len(requests))
    for i, req := range requests {
        results[i] = process(req) // wartet auf jedes Ergebnis bevor es weitergeht
    }
    return results
}

// Concurrent – Golang-Denken
func processRequests(requests []Request) []Result {
    results := make([]Result, len(requests))
    var wg sync.WaitGroup

    for i, req := range requests {
        wg.Add(1)
        go func(i int, req Request) {
            defer wg.Done()
            results[i] = process(req) // läuft parallel
        }(i, req)
    }

    wg.Wait()
    return results
}

Der zweite Unterschied war idiomatisches Go. PHP-OOP-Patterns funktionieren in Golang nicht. Vererbungshierarchien, abstrakte Klassen, magische Methoden – das gibt es in Golang nicht und aus gutem Grund. Interfaces in Golang sind implizit. Komposition schlägt Vererbung. Wer versucht PHP-OOP in Golang zu übersetzen kämpft gegen die Sprache statt mit ihr.

Der Shift hat Zeit gebraucht. Pair Programming half mehr als Dokumentation. Wer einmal gesehen hat wie ein concurrent Golang-Service unter Last reagiert versteht intuitiv warum das Modell so funktioniert wie es funktioniert – und will nie wieder zurück.

Observability als operatives Fundament

Die zweite große Herausforderung war Observability. In einem gemischten Stack aus PHP und Golang auf AWS wird es komplizierter als in einem homogenen Stack.

Das Problem ist Konsistenz. Wenn ein Request von einem PHP-Frontend-Service zu einem Golang-Auth-Service zu einem Golang-Payment-Service läuft muss die Trace-ID durch alle drei Services mitgeführt werden. Ohne das ist Debugging unter Last Detektivarbeit.

OpenTelemetry hat das gelöst – als gemeinsamer Standard über beide Sprachen hinweg. In Golang sieht die Trace-Propagation über gRPC so aus:

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

conn, err := grpc.NewClient(
    target,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)

PHP-Services und Golang-Services exportieren Traces im selben Format. Datadog sammelt alles ein. Ein Request durch das gesamte System ist als einzelner Trace sichtbar unabhängig davon welche Sprache welchen Teil verarbeitet hat.

Ohne diese Sichtbarkeit ist ein gemischter Stack schwerer zu betreiben als ein homogener – egal wie gut die einzelnen Services funktionieren. Observability ist kein Nice-to-have. Es ist die Voraussetzung dafür dass der gemischte Stack operativ beherrschbar bleibt.

Was am Ende zählt

PHP und Golang schließen sich nicht aus. Sie ergänzen sich. Aber nur wenn man aufhört in einer Sprache zu denken – und anfängt in Systemen zu denken.

Der schwierigste Teil war nie der Code. Es war das Loslassen von Mustern die jahrelang funktioniert hatten. Das Request-Response-Denken das so tief verankert ist dass es erst sichtbar wird wenn es nicht mehr funktioniert. Die OOP-Muster die in PHP elegant sind und in Golang gegen die Sprache arbeiten. Das Concurrency-Modell das man nicht lesen kann – man muss es erleben.

Ein gemischter Stack aus PHP und Golang ist keine Notlösung. Er ist eine bewusste Architekturentscheidung die das Beste aus beiden Welten nutzt. PHP bleibt dort wo es stark ist. Golang übernimmt dort wo Performance und Concurrency zählen. gRPC sorgt dafür dass beide Welten sauber miteinander kommunizieren.

Was bleibt ist ein System das skaliert ohne neu geschrieben werden zu müssen.