Zurück zum Blog

Warum du von PHP zu Go migrieren solltest

PHP blockiert für jeden Request einen kompletten Worker, während ein einzelner Go-Prozess Tausende Verbindungen gleichzeitig halten kann. Dieser Unterschied in der Nebenläufigkeit ist der eigentliche Grund, warum Teams migrieren, und du kannst einen Service nach dem anderen umziehen, statt alles neu zu schreiben.

Warum du von PHP zu Go migrieren solltest

Wenn PHP unter Last an seine Grenzen kommt, liegt das selten an der reinen Geschwindigkeit der Sprache. Das Problem ist, wie sie mit gleichzeitigen Requests umgeht. Unter PHP-FPM belegt jeder Request für seine gesamte Dauer einen vollständigen Worker-Prozess. Jeder Request, der seine Zeit mit Warten verbringt, etwa auf eine langsame Query, einen Aufruf bei einem Drittanbieter oder eine Verbindung, die offen bleibt, blockiert also einen Worker, der sonst jemand anderen bedienen könnte. Echtzeit-Features wie Live-Dashboards, Streaming-Responses und APIs mit hohem Fan-out stoßen schnell an diese Grenze. Mehr Worker oder mehr Maschinen schieben die Obergrenze nach hinten, aber sie bleibt bestehen.

Das Nebenläufigkeitsmodell von Go hat diese Obergrenze nicht, und das ist der Hauptgrund, warum Teams von PHP zu Go wechseln. Dieser Beitrag behandelt, warum das Modell wichtig ist, was es für Deployment und Kosten bedeutet, was Teams nach dem Umstieg berichtet haben und wie du einen Service nach dem anderen migrierst, statt das ganze Produkt auf einen Rewrite zu setzen.

Kurzfassung

  • Migriere wegen des Nebenläufigkeitsmodells. Das Process-per-Request-Design von PHP blockiert für die gesamte Lebensdauer eines Requests einen kompletten Worker. Mit Goroutines jongliert ein einzelner Go-Prozess Tausende gleichzeitige Verbindungen. Der Abstand wächst mit jedem Echtzeit- und verbindungsintensiven Feature, das du hinzufügst.
  • Der Unterschied beim Speicher ist groß. Ein PHP-FPM-Worker belegt grob 30 bis 60 MB, sobald dein Framework geladen ist. Eine Goroutine startet mit etwa 8 KB. Fünfzig gleichzeitige Streams können einen FPM-Pool auslasten, während ein einzelner Go-Prozess sie kaum bemerkt.
  • Teams, die umgestiegen sind, berichten von großen Verbesserungen. Engineers von Cloudflare und dem Fintech Curve beschreiben einen Service, der nach dem Umstieg auf Go von rund 10 Requests pro Sekunde auf Tausende kam. Neue Engineers waren nach ein bis zwei Wochen produktiv (Go Time #316).
  • Das Deployment ist einfacher. Go kompiliert zu einem einzigen statischen Binary. Du installierst keine Runtime, führst kein Composer aus und tunst kein FPM. Das passt gut zu Microservices und Kubernetes.
  • Du musst nicht alles neu schreiben. Nutze das Strangler Pattern. Migriere den einen Service, der wehtut, betreibe ihn neben deinem PHP und weite die Migration aus, sobald er sich bewährt hat.

Inhaltsverzeichnis

Der eigentliche Grund für die Migration ist Nebenläufigkeit

Klassisches PHP arbeitet nach dem Prinzip Share-Nothing und Process-per-Request. Ein Request kommt an, PHP-FPM gibt ihn an einen Worker weiter, und der Worker führt deinen Code von einem sauberen Ausgangszustand bis zur Response aus und setzt sich danach zurück. Das Design ist einfach und gut isoliert. Der Preis dafür ist Speicher.

Ein einzelner PHP-FPM-Worker belegt typischerweise 30 bis 60 MB Resident Memory, sobald dein Framework geladen ist. 50 gleichzeitige Requests bedeuten 50 Worker und grob 1,5 bis 3 GB RAM, die größtenteils mit Warten verbracht werden.

Stell dir jetzt vor, diese 50 Requests sind Server-Sent-Events-Streams oder WebSocket-Verbindungen, die jeweils minutenlang offen bleiben. Fünfzig langlebige Verbindungen können einen kompletten FPM-Pool auslasten, und der 51. Nutzer muss warten. Mehr Hardware draufzuwerfen wird hier schnell teuer, weil jede gleichzeitige Verbindung einen ganzen Prozess kostet.

Go geht den umgekehrten Weg. Eine Goroutine, die Einheit der Nebenläufigkeit in Go, startet mit einem Stack von etwa 8 KB, und die Runtime verteilt Tausende davon auf einen kleinen Pool von OS-Threads. Blockiert eine Goroutine bei I/O, parkt der Scheduler sie und führt auf demselben Thread eine andere aus. Ein einzelner Go-Prozess kann Zehntausende offene Verbindungen halten und braucht dabei nur einen Bruchteil des Speichers, den eine vergleichbare FPM-Flotte benötigen würde. Ein Connection-Handler ist einfach eine ganz normale Funktion:

example.gogo
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
	// Each connection is one cheap goroutine. Blocking here parks
	// this goroutine and frees the thread for others, no pool to exhaust.
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/event-stream")

	for event := range s.events(r.Context()) {
		fmt.Fprintf(w, "data: %s\n\n", event)
		flusher.Flush()
	}
}

Der net/http-Server führt jeden Request automatisch in einer eigenen Goroutine aus. Dieser Code hält also eine aktive Streaming-Verbindung, ohne einen schwergewichtigen Worker zu reservieren. Wenn sich dein Produkt in Richtung Echtzeit-Features, API-Gateways mit hohem Fan-out oder Services mit vielen gleichzeitigen Verbindungen bewegt, entscheidet dieser Unterschied viel. Das Modell von PHP arbeitet gegen solche Workloads, und Go bewältigt sie ohne besonderen Aufwand. Falls Goroutines und Channels neu für dich sind, führt dich der Kurs Go Concurrency Fundamentals praktisch durch das Modell.

Was Teams nach dem Umstieg gewinnen

Der beste Bericht aus erster Hand über eine echte Migration von PHP zu Go ist Folge 316 des Podcasts Go Time. Engineers von Cloudflare und dem Fintech Curve sprechen darin darüber, wie sie PHP-Systeme in Produktion auf Go umgestellt haben (Go Time #316, Changelog). Hier beschreiben Praktiker ihre eigenen Systeme, und das macht die Quelle nützlicher als eine Case Study eines Anbieters oder einen Blogbeitrag, der Zahlen von außen schätzt.

Ein Service kam Berichten zufolge von rund 10 Requests pro Sekunde im alten Setup auf Tausende nach dem Umstieg. Die Runtime fassten sie mit „selbst schlechtes Go ist unglaublich performant“ zusammen. Du bekommst also viel Durchsatz, lange bevor du überhaupt optimieren musst. Neue Engineers waren nach ein bis zwei Wochen produktiv statt erst nach Monaten, und das liegt daran, wie klein der Sprachumfang von Go ist.

Die Go-Wurzeln von Curve reichen zurück zu Monzo, einem weiteren Fintech, das stark auf Go gesetzt hat. So hat sich das Muster in dieser Branche verbreitet.

Es gibt auch andere Datenpunkte zu Go in Produktion. HelloFresh hat sein API-Gateway Janus in Go gebaut und als Open Source veröffentlicht (Janus auf GitHub). Ein Gateway ist ein Routing-Layer mit hohem Fan-out, genau die Art von Workload, bei der Goroutines am meisten bringen. Du findest außerdem viele einzelne Engineers, die berichten, dass ein Batch-Job, den sie von PHP nach Go portiert haben, deutlich schneller läuft und dabei nur einen Bruchteil der Ressourcen braucht.

Go vs PHP: Die technischen Vorteile

So schneiden die beiden bei den Punkten ab, die für einen Backend-Service zählen.

AspektPHPGo
NebenläufigkeitProcess-per-Request, wobei Fibers Nebenläufigkeit bringen, aber keine ParallelitätGoroutines auf einem M:N-Scheduler, echte Parallelität über alle Kerne
TypisierungDynamisch, schrittweise ergänzbare Type HintsStatisch, zur Compile-Zeit geprüft
DeploymentCode + Composer-Abhängigkeiten + FPM + WebserverEin statisches Binary, keine Runtime zu installieren
AusführungInterpretiert, mit JIT-UnterstützungZu nativem Maschinencode kompiliert

Die Nebenläufigkeit zählt am meisten. PHP 8.1 hat Fibers eingeführt, und die sind eine echte Verbesserung. Es lohnt sich aber, genau zu sein, was sie leisten. Fibers bieten kooperative Nebenläufigkeit, um asynchrones I/O zu strukturieren. Mit ihnen kann ein Worker wartende Tasks abwechselnd bearbeiten. Sie ermöglichen es aber nicht, dass ein PHP-Prozess 16 Kerne mit CPU-Arbeit auslastet, so wie es der Scheduler von Go tut.

Go verteilt viele Goroutines auf viele OS-Threads, führt sie parallel aus und parkt sie günstig, sobald sie blockieren. Dasselbe Primitiv liefert dir günstige Nebenläufigkeit und echte Parallelität. Ein Worker-Pool ist ein gutes Beispiel. Du greifst in Go ständig zu einem, und im klassischen PHP gibt es kein sauberes Äquivalent innerhalb eines einzelnen Prozesses:

example.gogo
func process(jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range jobs {
		results <- job.Run() // each worker pulls the next job when free
	}
}

func main() {
	jobs := make(chan Job, 100)
	results := make(chan Result, 100)
	var wg sync.WaitGroup

	// Fan out to 8 workers sharing one queue, all in one process.
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go process(jobs, results, &wg)
	}

	go func() {
		for _, j := range loadJobs() { // pull the batch from your work source
			jobs <- j
		}
		close(jobs)
	}()

	go func() { wg.Wait(); close(results) }()
	for r := range results {
		record(r)
	}
}

Das Deployment bekommt weniger Aufmerksamkeit, als es verdient. Ein Go-Build erzeugt ein einziges statisches Binary, und die Auslieferung kann so einfach sein, wie diese Datei auf einen Server oder in einen Scratch-Container zu kopieren. Es gibt keine PHP-Runtime zu installieren, keinen Composer-Schritt, kein FPM-Tuning und keine Webserver-Konfiguration davor. Wenn du Microservices in Kubernetes betreibst, hast du bei jedem Deploy dieses kleine, in sich geschlossene Artefakt.

Statische Typisierung hilft ebenfalls. Der Compiler findet schon beim Build eine ganze Klasse von Fehlern, die PHP erst zur Laufzeit zeigt, und das wird umso wichtiger, je größer ein Service wird und je mehr Leute daran arbeiten.

Dann gibt es noch den Einwand „modernes PHP ist doch inzwischen schnell“. PHP hat den Rückstand bei der reinen Geschwindigkeit tatsächlich zu großen Teilen aufgeholt, mit dem JIT in PHP 8 und Worker-Runtimes wie FrankenPHP und RoadRunner, die deine App über Requests hinweg gebootet im Speicher halten. Aber FrankenPHP und RoadRunner sind beide in Go geschrieben. Als das PHP-Ökosystem Application Server für hohe Nebenläufigkeit brauchte, baute es sie auf der Runtime von Go. Wenn Nebenläufigkeit dein Problem ist, kannst du eine Go-basierte Runtime vor PHP einsetzen. Oder du schreibst den verbindungsintensiven Service direkt in Go und sparst dir die Zwischenschicht.

Die Wahrheit über Benchmarks (und der echte Gewinn)

Du wirst auf Schlagzeilen stoßen, laut denen Go 10- bis 30-mal schneller ist als PHP. Solche Zahlen stammen aus synthetischen Suites wie den TechEmpower-Benchmarks, die Runtimes unter idealisierten, CPU- und Framework-gebundenen Bedingungen testen (TechEmpower Framework Benchmarks). Die Zahlen sind echt, aber sie sagen nichts über deine Latenz in Produktion voraus. Migriere also nicht in der Erwartung dieses Faktors.

In Produktion geht der Großteil der Zeit eines Requests an die Datenbank und das Netzwerk, und darauf warten Go und PHP gleich lange. Der Faktor schrumpft schnell. Teams, die nach einer Migration ehrlich messen, berichten oft von Verbesserungen im einstelligen bis niedrigen zweistelligen Prozentbereich bei der durchschnittlichen End-to-End-Latenz.

Der Durchschnitt verdeckt, wo Go gewinnt, nämlich bei der Stabilität der Tail-Latenz und den Kosten pro Request. Die p99- und p99.9-Latenzen von Go bleiben unter Last flach, weil kein Request auf einen freien FPM-Worker warten muss. Du bedienst denselben Traffic außerdem mit weit weniger Maschinen, da ein Prozess Nebenläufigkeit auffängt, die ein Pool nicht bewältigt. Diese Gewinne bemerkst du in Produktion und auf der Rechnung, und sie bleiben, lange nachdem sich niemand mehr für einen synthetischen Benchmark interessiert.

Schneller einstellen, schneller ausliefern

Auch Recruiting und Onboarding sprechen für Go. Der Sprachumfang ist klein, mit 25 Schlüsselwörtern und einem verbindlichen Formatter. Ein kompetenter Engineer, der von PHP oder einer anderen Sprache kommt, ist deshalb in ein bis zwei Wochen produktiv. Das deckt sich mit der Onboarding-Zeit, die die Engineers von Cloudflare und Curve beschrieben haben.

Du musst also keine Leute einstellen, die Go schon können. Sie lernen es schnell, und die erzwungene Formatierung und die statische Typisierung halten ihren Code vom ersten Tag an konsistent.

Außerdem ist Go in Marktumfragen oft mit einem Gehaltsaufschlag verbunden, was darauf hindeutet, dass die Nachfrage das Angebot übersteigt. Die genauen Zahlen, die aus Quellen wie Glassdoor zitiert werden, schwanken je nach Region und Rolle. Sieh sie deshalb als Rückenwind beim Recruiting, nicht als Posten in einem Business Case. Am stärksten ist das Ökosystem von Go bei Networking, Infrastruktur, CLIs und Services mit hohem Durchsatz, und genau diese Arbeit würdest du migrieren. Goroutines wurden für diese Arbeit gebaut, und die Bibliotheken und das Tooling drumherum sind ausgereift.

Migrieren, ohne die Firma aufs Spiel zu setzen

Schreib nicht alles auf einmal neu. Komplette Rewrites gehören zu den zuverlässigsten Wegen, ein Produkt zum Stillstand zu bringen, denn funktionierender Code enthält jahrelange Bugfixes und Edge Cases, die du wegwerfen würdest (Joel on Software). Die Teams von Cloudflare und Curve haben auch keinen Big-Bang-Rewrite gemacht. Sie haben das Strangler Pattern eingesetzt, und das lohnt sich zu übernehmen.

Fang mit dem einen Service an, der wirklich Probleme macht: dem SSE-Stream, der deinen Pool auslastet, der Fan-out-API, die deine Worker blockiert, oder dem Gateway, das nicht genug Verbindungen halten kann. Baue nur diesen Service in Go neu, stelle ihn hinter denselben Routing-Layer und betreibe ihn neben deinem PHP.

Leite einen Teil des Traffics dorthin, beobachte Speicherverbrauch und Tail-Latenz, bevor du mehr umleitest. Dein PHP bedient die ganze Zeit über alles andere. Es gibt also keinen Stichtag für eine Komplettumstellung, und nichts hängt an einem einzigen Umschalten. Wenn sich dieser erste Service bewährt, was er meistens tut, hast du echte Zahlen und eine Vorlage für den nächsten.

Damit wird die Migration zu einer Entscheidung pro Service. Du musst nicht zur „reinen Go-Firma“ werden. Behalte PHP dort, wo es passt, etwa für das CMS, die Content-Seiten oder das Laravel-Admin, mit dem dein Team zügig ausliefert. Verlagere die verbindungsintensiven Services mit hohem Durchsatz nach Go.

Häufig gestellte Fragen

Ist Go schneller als PHP?

In synthetischen, CPU-gebundenen Benchmarks ja, oft um ein Vielfaches (TechEmpower). In Produktion ist der Unterschied bei der durchschnittlichen Latenz meist kleiner, weil der Großteil der Zeit eines Requests an die Datenbank und das Netzwerk geht, und darauf warten beide Sprachen gleich lange. Go behält seinen Vorsprung bei Nebenläufigkeit und Tail-Latenz unter hoher Last. Es hält Tausende Verbindungen in einem Prozess und die p99-Latenz flach, wo einem FPM-Pool die Worker ausgehen würden. Du brauchst also weniger Maschinen für denselben Traffic.

Sollte ich meine komplette PHP-App in Go neu schreiben?

Nein. Migriere schrittweise. Komplette Rewrites gehören zu den zuverlässigsten Wegen, ein Produkt zum Stillstand zu bringen (Joel on Software). Nutze das Strangler Pattern: Verlagere den einen problematischen Service mit hoher Nebenläufigkeit nach Go, betreibe ihn neben deinem PHP, leite den Traffic nach und nach um und weite die Migration aus, sobald er sich bewährt hat. So bekommst du die Vorteile ohne eine riskante Umstellung an einem Stichtag.

Welche Services sollte ich zuerst migrieren?

Fang mit den verbindungsintensiven Services und denen mit hohem Fan-out an: WebSocket- und SSE-Endpoints, Echtzeit-Features, API-Gateways, Streaming-Responses und jeder Service, bei dem viele Requests offen bleiben und auf I/O warten. Bei diesen stößt das Process-per-Request-Modell von PHP zuerst an seine Grenzen, und Goroutines helfen sofort. Deshalb liefern sie dir am schnellsten den Beleg, dass sich die Migration auszahlt.

Kann PHP mit WebSockets und Echtzeit-Features umgehen?

Ja, mit Unterstützung. Klassisches PHP-FPM blockiert pro Verbindung einen Worker, sodass viele langlebige Verbindungen den Pool erschöpfen. Runtimes wie Swoole und FrankenPHP halten Worker am Leben und ergänzen ereignisgesteuerte Nebenläufigkeit, damit das funktioniert. Go bewältigt dieselbe Last nativ, mit einer günstigen Goroutine pro Verbindung. Verbindungsintensive Services der häufigste Grund, warum Teams migrieren.

Ist Go schwerer zu lernen als PHP?

Die Syntax nicht. Go hat nur 25 Schlüsselwörter und einen verbindlichen Formatierungsstil, und die meisten Engineers sind innerhalb von ein bis zwei Wochen produktiv. Neu ist für PHP-Entwickler das Nebenläufigkeitsmodell (Goroutines und Channels) sowie die Fehlerbehandlung über Rückgabewerte statt Exceptions. Das braucht etwas Übung, und genau diese Fähigkeiten wirst du in den migrierten Services am häufigsten brauchen.

Bereit, produktionsreifes Go zu schreiben?

Am besten beurteilst du Go, indem du selbst Go schreibst. Bei LevelUpGo ist jede Lektion eine praxisnahe Übung, die du im Browser löst. Dein Code läuft auf einer echten Go-Toolchain und wird gegen die Tests der Übung geprüft. So bekommst du sofort Feedback zu funktionierendem Go-Code, statt nur darüber zu lesen.

Ein Lernweg für PHP-Entwickler:

  • Go Fundamentals behandelt Typen, Structs und Fehlerbehandlung, also die Teile, die sich anders anfühlen, wenn du von PHP kommst. Der Einstieg ist kostenlos und braucht keine Kreditkarte.
  • Composite Types behandelt Slices, Maps und Structs, die alltäglichen Bausteine für die Daten, die durch einen Service fließen.
  • Go Concurrency Fundamentals bringt dir Goroutines, Channels und select bei, also die Features, mit denen ein einzelner Go-Prozess schafft, was ein ganzer PHP-FPM-Pool nicht kann.
  • Die Roadmap zeigt den Weg von deinem ersten Programm bis zu einem nebenläufigen Abschlussprojekt.

Weitere Vergleiche: Von Python zu Go wechseln, Go vs Rust 2026 und Go vs Node.js und die Frage der Supply-Chain-Sicherheit.

Quellen

In diesem Beitrag zitierte Quellen, Berichte von Praktikern aus erster Hand zuerst:

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen