Wer das Implementieren parallel laufender Aktionen anpeilt, sollte einen Blick auf die Programmiersprache Go werfen. Sie unterstützt von Haus aus Nebenläufigkeit und erleichtert mit klaren Konzepten vor allem das Entwickeln von Webservices – zumindest ein wenig.
Internet-Dienste müssen häufig mehrere Aufgaben gleichzeitig erledigen. Während etwa im Hintergrund ein hochgeladenes Foto auf seine Ablage in einem Speicherdienst wartet, lässt sich parallel schon ein Vorschaubild generieren. Solche nebenläufigen Aufgaben zu implementieren, gestaltet sich jedoch häufig schwierig. Das beliebte PHP beispielsweise führt traditionell alle Anweisungen strikt nacheinander aus. Asynchron zu programmieren erlaubten erst Frameworks wie Amp [1] oder Swoole [2]. Seit PHP 7.2 existiert die offizielle Erweiterung »parallel« [3], PHP 8.1 brachte Fibers [4]. In jedem Fall muss man sich beim Programmieren mit den jeweiligen Eigenheiten der Frameworks und Implementierungen herumschlagen.
Einen etwas anderen Weg geht Golang. Dessen Entwickler bauten die asynchrone Programmierung direkt in die Sprache ein. Ein einziges zusätzliches Schlüsselwort genügt, um eine Funktion parallel ausführen zu lassen. Die Konzepte haben sich bereits in der Praxis bewährt, wie unter anderem Owncloud beweist: Seine neue Infinite-Scale-Plattform nutzt intensiv die nebenläufigen Fähigkeiten von Go [5]. Inzwischen greifen sogar zahlreiche andere Sprachen die Ideen aus Golang auf. Es lohnt sich also, die Konzepte dahinter genauer anzusehen. Die dazu im Folgenden aufgeführten Beispiele setzen keine Kenntnisse in Go voraus. Beherrschen Sie bereits eine Programmiersprache, sollten Sie den Quellcode problemlos verstehen.
Schlafenszeit
Zunächst kann Go das Ausführen von Funktionen verzögern. Stellen Sie einem Funktionsaufruf das Schlüsselwort »defer« voran, parkt Go die Funktion auf einem Stack. Sobald die umgebende Funktion abgearbeitet ist, ruft es nacheinander die Funktionen vom Stack auf (Listing 1).
Listing 1
Funktionen verzögern
package main
import "fmt"
func main() {
defer fmt.Println("Welt!")
fmt.Println("Hallo")
}
Durch das »defer« wandert in diesem Beispiel »fmt.Println(“Welt!”)« unbearbeitet auf den Stack. Die nächste Funktion führt Go normal aus, und auf dem Bildschirm erscheint »Hallo«. Damit ist die Funktion »main()« komplett abgearbeitet, und Go startet die Funktionen vom Stack. Dort liegt »fmt.Println(“Welt!”)«, das »Welt!« ausgibt (Abbildung 1). Letztlich verschiebt »defer« lediglich Aktionen auf einen späteren Zeitpunkt und bietet daher keine echte Asynchronität.
Kickstart
Stellen Sie einer Funktion bei ihrem Aufruf das Schlüsselwort »go« voran, erledigt sie ihre Arbeit parallel zum Hauptprogramm. So würde »go countdown(10)« parallel zum restlichen Geschehen von 10 bis 1 zählen (Abbildung 2)
Listing 2 zeigt die Funktion als komplettes Programm. Unter der Haube stößt das Schlüsselwort »go« eine Goroutine an. Die Go-Runtime besitzt einen eigenen Scheduler, der die Goroutinen verwaltet und intelligent auf Betriebssystem-Threads verteilt. Innerhalb eines solchen Threads läuft dann die hinter »go« angegebene Funktion ab, im Beispiel »countdown()«.
In einer realen Webapplikation könnte eine solche Goroutine beispielsweise eine Datei auf Viren prüfen. Auch die Infinite-Scale-Plattform von Owncloud nutzt das Konzept: Sie besteht aus zahlreichen Microservices, die jeweils in einer eigenen Goroutine laufen. Durch die Arbeitsweise von Go kann man allerdings nie sicher sein, wann welche Goroutine ihre Aktionen ausführt.
Listing 2
Einfache Goroutine
package main
import (
"fmt"
"time"
)
func countdown(c int) {
for i := c; i > 0; i-- {
time.Sleep(time.Second)
fmt.Println(i)
}
}
func main() {
go countdown(10)
fmt.Println("Countdown:")
time.Sleep(11 * time.Second)
}
Volkszählung
Beenden Sie ein Go-Programm, reißen Sie unweigerlich auch alle laufenden Goroutinen in den Tod. Aus diesem Grund wartet »main()« aus Listing 2 am Ende noch 11 Sekunden. Ohne diese Zeile würde das Programm die Goroutine »countdown()« starten und sich dann sofort beenden. »countdown()« hätte somit keinerlei Möglichkeit, auch nur eine Zahl auszugeben.
Die Implementierung wirkt allerdings nicht besonders elegant: Während man die Laufzeit von »countdown()« noch abschätzen kann, dürfte sie bei den meisten Goroutinen unbekannt sein. Hier springen die WaitGroups aus der Standardbibliothek ein, mit denen Sie alle laufenden Goroutinen im Blick behalten (Abbildung 3). Listing 3 demonstriert, wie sie sich nutzen lassen, und startet drei parallele Countdowns.
Listing 3
WaitGroup
package main
import (
"fmt"
"time"
"sync"
)
func countdown(c int, wg *sync.WaitGroup) {
defer wg.Done()
for i := c; i > 0; i-- {
time.Sleep(time.Second)
fmt.Println(i)
}
}
func main() {
var w sync.WaitGroup
for i := 0; i < 3 ; i++ {
w.Add(1)
go countdown(10, &w)
}
fmt.Println("Countdown:")
w.Wait()
}
WaitGroup zählt lediglich die laufenden Goroutinen. Listing 3 initialisiert in einer Schleife drei Goroutinen, wobei es jedes Mal den WaitGroup-Zähler in »w« per »w.Add(1)« um eins erhöht. Beendet die Funktion »countdown()« einen Countdown, zieht sie durch »wg.Done()« wieder eins ab. Am Ende des Programms hält »w.Wait()« die Verarbeitung so lange an, bis der Zähler wieder auf 0 steht und somit keine Goroutinen mehr im Hintergrund laufen. Im Beispiel lässt sich mit einer anonymen Funktion zusätzlich die Deklaration von »countdown()« einsparen, wobei der Code aus Listing 4 die Zeile »go countdown(10, &w)« ersetzt.
Listing 4
countdown() einsparen
go func(c int) {
defer w.Done()
for j := c; j > 0; j-- {
time.Sleep(time.Second)
fmt.Println(j)
}
} (10)
Mit einer WaitGroup verlieren Sie zwar die laufenden Goroutinen nicht aus den Augen, verzählen sich allerdings mitunter recht schnell – zum Beispiel sobald sich eine Goroutine versehentlich nicht per »Done()« abmeldet. Besonders in der Testphase müssen Sie deswegen penibel auf solche Fälle achten.
Vor allem Webdienste und Microservices sollen immer zur Verfügung stehen. Beendet sich plötzlich eine Goroutine, sollte das Programm sie neu starten. Mit den WaitGroups fällt eine derartige Überwachung weg. Hier springen Packages von Drittanbietern wie Suture [6] in die Bresche. Suture verwendet einen aus Erlang bekannten Supervisor Tree. Dabei überwacht ein Supervisor die übrigen Goroutinen und startet sie bei Bedarf neu.
Ampelsteuerung
In der Praxis müssen oft mehrere Goroutinen auf dieselben Daten zugreifen. Da sie nebeneinander herlaufen, kann das in ein Datenchaos münden (Abbildung 4). Berechnet etwa die Goroutine gerade den aktuellen Stand des Bankkontos neu, würde das parallel weiterlaufende Hauptprogramm noch den alten Stand anzeigen und so dem Kunden mehr Geld versprechen, als er eigentlich besitzt. Für solche Race Conditions bietet Go zwei Lösungsansätze.

Abbildung 4: Man kann nie abschätzen, wann eine Goroutine ihre Arbeiten erledigt. Hier führt das zu asynchronen Ausgaben.
Zunächst können mehrere Goroutinen eine Variable gemeinsam nutzen. Um dabei das Chaos einzudämmen, stellt Go neben der Variablen eine Ampel auf. Sobald eine Goroutine auf die Variable zugreifen möchte, stellt sie die Ampel auf Rot. Schaltet sie die Ampel nach ihrer Arbeit wieder auf Grün, darf eine andere Goroutine auf die Variable zugreifen. Wie viele anderen Sprachen bezeichnet Go solche Ampeln als Mutex, das Prinzip dahinter heißt Mutual Exclusion (wechselseitiger Ausschluss).
Ein Mutex lässt sich bequem über das Package sync verwalten (Listing 5). Dort zählt die Funktion »countdown()« die Variable »v« hoch. Parallel möchte die Funktion »current()« den jeweils aktuellen Wert ausgeben. Damit sich die Kontrahenten nicht ins Gehege kommen, sperrt zunächst die Funktion »countdown()« den zugehörigen Mutex »mu« über »mu.Lock()«. Mit anderen Worten schaltet die Funktion die Ampel mit dem Namen »mu« auf Rot. »countdown()« zählt danach »v« hoch und entsperrt schließlich mit »mu.Unlock()« den Mutex. Damit zeigt die Ampel »mu« wieder Grün, und »current()« könnte auf die Variable zugreifen.
Bevor die Funktion »current()« den jeweils aktuellen Wert ausgibt, sperrt auch sie sicherheitshalber den Mutex »mu«. Damit kann »countdown()« nicht genau in diesem Moment den Wert in »v« überschreiben. »defer« sorgt hier elegant dafür, dass der Mutex auf jeden Fall am Ende der Funktion entsperrt wird. Durch diesen Kniff vergessen Sie vor allem bei längeren Funktionen nicht, den Mutex im richtigen Moment wieder zu öffnen.
In einem Go-Programm dürfen beliebig viele Mutexe beziehungsweise Ampeln existieren. Damit Sie wissen, welcher Mutex welche Daten schützen soll, fassen Sie diese wie in Listing 5 in einer entsprechenden Structure zusammen.
Listing 5
Mutexe einsetzen
package main
import (
"fmt"
"time"
"sync"
)
type Counter struct {
mu sync.Mutex
v int
}
func countdown(c *Counter) {
c.mu.Lock()
c.v--
c.mu.Unlock()
}
func current(c *Counter) {
c.mu.Lock()
defer c.mu.Unlock()
fmt.Println(c.v)
}
func main() {
var c Counter
c.v = 10
for i := c.v; i > 1; i-- {
go countdown(&c)
go current(&c)
}
time.Sleep(time.Second)
}
Verkehrsverstöße
Eine reale rote Ampel könnte man jederzeit überfahren. Analog kann eine Goroutine den Mutex ignorieren, etwa durch einen Programmierfehler. Wie im wahren Leben zieht diese Missachtung häufig weitreichende Folgen nach sich. Sie sollten deswegen genau einen Mutex für ein Paket aus zusammenhängenden Daten verwenden und dessen Nutzung penibel sicherstellen.
Des Weiteren sollte eine Goroutine den Zugriff auf die Variable schnellstmöglich wieder freigeben, da sie sonst ihre Kolleginnen blockiert. Im schlimmsten Fall warten alle Goroutinen gleichzeitig darauf, dass sie endlich eine Variable nutzen dürfen. Um einen solchen Deadlock zu verhindern, sollten Sie den Code nach dem Sperren des Mutex einfach und kurz halten und unbedingt ausführlich testen.
Dabei hilft Go ein wenig: So erkennt die Runtime Deadlocks und bricht das Programm mit einer entsprechenden Fehlermeldung ab (Abbildung 5). Geben Sie dem Compiler den Parameter »-race« mit auf den Weg, aktiviert er zusätzlich den Race Detector. Der verfolgt unter anderem die gemeinsam genutzten Variablen sowie die Aufrufe von »sync.Mutex« und »sync.WaitGroup«. Sofern der Race Detector bei der Ausführung ein Problem erkennt, generiert er einen ausführlichen Report.

Abbildung 5: Sollten alle Goroutinen nicht mehr weiterarbeiten können, bricht die Go-Runtime automatisch das komplette Programm ab.
Rohrleitungsbau
Goroutinen können Informationen über Channels austauschen. Bildlich können Sie sich einen Channel wie ein Rohr vorstellen: An einem Ende schiebt die Goroutine ihre berechneten Daten hinein, die dann eine Kollegin oder das Hauptprogramm am anderen Ende entgegennimmt. Die Infinite-Scale-Plattform von Owncloud setzt solche Channels unter anderem beim Verbindungsaufbau mit LDAP-Servern und zum Sammeln von Log-Informationen ein.
Um das über Channels bereitgestellte Rohrpostsystem in Betrieb zu nehmen, erstellen Sie zunächst ein neues Rohr mit der eingebauten Funktion »make()«. Im Beispiel aus der ersten Zeile von Listing 6 entsteht der Channel »chan«, der ausschließlich Ganzzahlen transportiert (»int«). Diesen Channel geben Sie anschließend an die Goroutine weiter (Zeile 2). Die wiederum übernimmt das Berechnen und schiebt dann ihr Ergebnis mit dem Left-Arrow-Operator »<-« in das Rohr »c« (Zeile 3 bis 6). Die umgehend durch den Channel gerutschte Zahl kann jetzt eine andere Goroutine auf dieselbe Weise abholen. Der Pfeil-Operator zeigt dabei vom Channel weg, Sie können an ihm also die Flussrichtung der Daten ablesen (Zeile 7).
Listing 6
Channels verwenden
c := make(chan int)
go countdown(10, c)
func countdown(a int, c chan int) {
res := a--
c <- res
}
r := <- c
fmt.Println(r)
In Listing 7 finden Sie das Vorgehen noch einmal als vollständiges Programm. Nachdem die Goroutine etwas in den Channel gestopft hat, wartet sie, bis jemand die abgegebenen Daten am anderen Ende angenommen hat. Gleiches gilt in umgekehrter Richtung: Das Hauptprogramm verharrt so lange bei der Anweisung »r := <- c«, bis die Goroutine endlich etwas in den Channel hineingeworfen hat. Anders gesagt blockiert ein Channel immer die Ausführung.
Das Warten erweist sich beispielsweise dann als nützlich, wenn Sie wichtige Daten an einen Online-Speicherpool weiterreichen und auf dessen Schreibbestätigung warten müssen. Sollten jedoch irgendwann alle Parteien auf die Antwort der jeweils anderen warten, würde erneut ein Deadlock entstehen. Einen solchen können Sie testweise provozieren, indem Sie in Listing 7 die Zeilen 11 und 12 miteinander vertauschen.
Listing 7
Einfacher Channel
package main
import "fmt"
func countdown(a int, c chan int) {
res := a - 1
c <- res
}
func main() {
c := make(chan int)
for i := 11; i > 1; i-- {
go countdown(i, c)
r := <- c
fmt.Println(r)
}
}
Kanäle mit Gedächtnis
Ein Channel kann mehrere Informationen weiterleiten. Wie viele Daten durch das Rohr passen, legen Sie beim Erstellen fest. Der Channel »c« aus der ersten Zeile von Listing 8 kann beispielsweise zwei Ganzzahlen entgegennehmen. Ein solcher Buffered Channel blockiert erst dann, wenn er tatsächlich voll ist. Wäre der Channel nach der zweiten Anweisung (dritte Zeile) voll, würde Go hier die Verarbeitung anhalten, bis jemand anderes den Channel wieder geleert hat.
Listing 8
Buffered Channel
c := make(chan int, 2) c <- 1 c <- 2 c <- 3
Die Inhalte in einem gepufferten Channel purzeln in der Reihenfolge heraus, in der sie in den Channel eingeflossen sind (FIFO-Prinzip). »r := <-c« liefert folglich erst einmal nur die »1«, die nächste Zahl im Channel holt ein erneuter Aufruf von »<-c«.
Ein Praxisbeispiel für Buffered Channels zeigt Listing 9, das gleichzeitig bei Google und Bing eine Suchanfrage stellt. Die Antworten schieben die zuständigen Goroutinen als Strings in den Channel »results«. Dessen Inhalte kann das Programm weiterverarbeiten und zum Beispiel in einer For-Schleife ausgeben (ab Zeile 6).
Listing 9
Buffered Channels nutzen
urls := []string{
"http://google.de/search?q=golang",
"http://bing.com/search?q=golang",
}
results := make(chan string, 2
for _, url := range urls {
go func(url string) {
res, _ := http.Get(url)
defer res.Body.Close()
body, _ :=
ioutil.ReadAll(res.Body)
results <- string(body)
}(url)
}
Eine Goroutine kann allen Empfängern explizit anzeigen, dass sie keine Daten mehr in den Channel schiebt, indem sie ihn über die Funktion »close()« schließt. Ob ein Channel offen oder geschlossen ist, verrät der standardmäßig verfügbare zweite Rückgabewert von »<-«:
close(c) v, ok := <-c
Enthält »ok« den Wert »false«, hat jemand den Channel geschlossen. Ab jetzt sollte tunlichst niemand mehr Daten hineinschieben. Das löst sonst wie in Abbildung 6 gezeigt unweigerlich Panik aus und führt zum sofortigen Programmabbruch. Sie sollten also stets prüfen, ob ein Channel bereits geschlossen ist, bevor sie darauf zugreifen.

Abbildung 6: Hier hat jemand in einen bereits geschlossenen Channel eine weitere Zahl geschoben, was umgehend eine Panik auslöst.
Multiple Choice
Mit »switch« gehen Sie in Go verschachtelten If-Abfragen aus dem Weg. Für die Channels existiert das Pendant »select«. Es wartet, bis sich eine der hinter den »case« notierten Anweisungen ausführen lässt (Listing 10).
Listing 10
select
select {
case outgoing <- x:
x++
case r := <-incoming:
fmt.Println(r)
default:
fmt.Println("Waiting ...")
}
Dabei prüft »select« immer alle Bedingungen gleichzeitig. Im Beispiel blockiert es die Verarbeitung, bis das Programm entweder eine Zahl in den Channel »outgoing« packen darf (»outgoing <- x«) oder der Channel »incoming« etwas enthält. Diese Aktion plus alle weiteren Anweisungen hinter dem Doppelpunkt führt Go dann aus. Im Zweifelsfall würde »select« unendlich lange warten, was das Schlüsselwort »default« jedoch unterbindet: Alle dahinter notierten Anweisungen führt Go immer dann aus, wenn keiner der anderen Fälle eintritt.
Reagiert ein Netzwerkdienst selbst nach einigen Sekunden nicht, leiten Sie Gegenmaßnahmen ein. Beim Umsetzen eines solchen Timeouts helfen »select« und die Funktion »time.After()« aus der Standardbibliothek. Sie erstellt einen Channel, der nach einer vorgegebenen Zeit einen Wert liefert. In Listing 11 wartet »select« auf diesen Wert, also bis der Timer abgelaufen ist.
Listing 11
Timeout einbauen
timeout := time.After(5 * time.Second)
select {
case answer := <-c:
fmt.Println("Response received")
case <-timeout:
fmt.Println("Timeout!")
}
Keksdosen
Cookies, API-Keys und viele weitere Informationen laufen aus Sicherheitsgründen nach ein paar Minuten ab. Für solche Daten bietet die Go-Standardbibliothek die Datenstruktur »context«, die Informationen speichert und sie bei Bedarf mit einer Lebensdauer versieht.
Dazu erstellen Sie zunächst einen Context (Listing 12, erste Zeile). Darin speichert der Aufruf aus der zweiten Zeile den Wert »123456« unter dem Schlüssel »cookie«. Die Funktion »context.WithTimeout()« setzt schließlich die Lebensdauer (Zeile 3). Im Beispiel bleiben die Daten nur eine Sekunde lang gültig.
Listing 12
Ein Context
ctx := context.Background()
ctx := context.WithValue(ctx, "cookie", "123456")
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
[...]
select {
case <- ctx.Done():
[...]
}
Ein »cancel()« sorgt dafür, dass der Context seine Ressourcen freigibt und einen eventuell noch laufenden Timer stoppt. Sie sollten es deswegen immer abschließend ausführen, was hier ein »defer« sicherstellt (Zeile 4). Den Ablauf der Zeit meldet der Context über einen Channel, an den Sie mit »ctx.Done()« gelangen. Sammeln sich darin Daten, ist die Zeit für das Cookie verstrichen. Diesen Fall fangen Sie bequem über »select« ab (ab Zeile 6).
Ein vollständiges Beispiel sehen Sie in Listing 13. Ein Context ist zwar recht simpel, beim Entwickeln von Diensten aber extrem hilfreich. Deswegen macht beispielsweise die Infinite-Scale-Plattform von Owncloud massiven Gebrauch davon.
Listing 13
Einen Context einsetzen
package main
import (
"fmt"
"time"
"context"
)
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "cookie", "123456")
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
fmt.Println(ctx.Value("cookie"))
select {
case <- ctx.Done():
fmt.Println("Cookie not valid")
}
}
Kanalarbeiten
Über die Channels reichen Sie Daten innerhalb der Anwendung unkompliziert weiter. So könnte beispielsweise eine Goroutine aus den Eingaben eines Benutzers sämtliche HTML-Tags entfernen und das Ergebnis dann über einen Channel an eine andere Goroutine weiterspielen, die ihrerseits den Text in einer Datenbank ablegt. Auf diese Weise lassen sich mit den Röhren elegant Pipelines aufbauen.
Daneben können Sie über Channels Signale und Kommandos an Goroutinen senden. So könnten Sie über eine entsprechende Nachricht die Goroutine am anderen Ende zum Abbrechen auffordern – ähnlich wie mit den Signalen, über die Sie Prozesse unter Linux steuern. Tatsächlich nutzen die Signalfunktionen aus der Go-Standardbibliothek den Channel »os/signal«, um solche Systemsignale abzufangen und zu verarbeiten.
Analog lassen sich Event Loops oder Event-Systeme realisieren: Wer auf ein Ereignis reagieren muss, zapft einen passenden Channel an, den wiederum eine Goroutine befüllt. Ist der Channel noch leer, wartet der Code. Verpacken Sie das in eine endlos laufende For-Schleife, entsteht ein Event Loop. Über Channels können Sie darüber hinaus Referenzen auf Datenstrukturen verschicken und die Verantwortung für die Daten abgeben. Nicht zuletzt lässt sich mithilfe von Buffered Channels der Datendurchsatz durch die Applikation steuern.
Hinter Channels steckt entsprechend ein recht universelles Konzept. In Go gilt sogar der Grundsatz: “Don’t communicate by sharing memory; share memory by communicating.” Dieses Prinzip ist mittlerweile auch in anderen Sprachen und Frameworks angekommen [7].
Fallstricke
Ergänzend zu den Goroutinen, Mutexen und Channels existieren weitere Frameworks und Bibliotheken, die vor allem das Entwickeln verteilt arbeitender Systemen vereinfachen. So setzt etwa Owncloud bei seiner Infinite-Scale-Plattform das Package go-micro ein, das beim Aufbauen und Betreiben der verteilten Microservice-Architektur hilft [8]. Viele Komponenten der Infinite-Scale-Plattform kommunizieren zudem über das GRPC-Framework [9].
Sowohl die Goroutinen als auch die Frameworks erleichtern jedoch lediglich die eigentliche asynchrone Programmierung. Die parallel ablaufenden Aktionen können Entwicklern weiterhin schlaflose Nächte bereiten. So rechnet eine komplexe Goroutine manchmal im Hintergrund unbemerkt munter weiter und frisst dabei langsam den Speicherplatz auf. Go schützt außerdem nicht vor den gefürchteten Deadlocks. Letztere versucht Owncloud über passende Daten-Sharing-Modelle und Mutexe zu verhindern. Zusätzlich sollten Sie jede Goroutine so gut wie möglich von den anderen abschirmen und den Datenaustausch auf ein Minimum reduzieren.
Das fast schon unberechenbare Verhalten der Anwendung erschwert vor allem das Testen. Besonders der Weg der Daten lässt sich gerade bei komplexeren Anwendungen nicht mehr einfach verfolgen. Klaas Freitag, CIO bei Owncloud, rät daher, parallel laufenden Code zu meiden wie der Teufel das Weihwasser. Asynchrone Programmierung sollte nur dann zum Einsatz kommen, wenn einerseits ein wirklich guter Grund vorliegt und Sie andererseits genau wissen, was Sie tun. In jedem Fall sollten Sie sich auf lange Debugging-Sitzungen einstellen.
Fazit
Goroutinen und Channels sind in Golang integriert, leichtgewichtig, intuitiv zu nutzen und sorgen für kompakten Code. Die Standardbibliothek liefert Entwicklern Mutexe und den wertvollen Context. Obendrein bringt die Go-Runtime eine Deadlock-Erkennung mit. Die Werkzeuge sind allerdings kein Allheilmittel: Nach wie vor können Deadlocks auftreten, und eine reibungslose Zusammenarbeit der Goroutinen verlangt ausführliche Tests. Dennoch greift Go Programmierern vor allem bei der Webentwicklung kräftig unter die Arme. (csi/jlu)
Infos
- Amp: https://github.com/amphp/amp
- Swoole: https://www.php.net/manual/de/intro.swoole.php
- Parallel: https://www.php.net/manual/de/intro.parallel.php
- Fibers: https://wiki.php.net/rfc/fibers
- Owncloud Infinite Scale: https://owncloud.com/de/infinite-scale/
- Suture: https://github.com/thejerf/suture
- PHP Parallel Philosophy: https://www.php.net/manual/de/philosophy.parallel.php
- Go-Micro: https://github.com/asim/go-micro
- GRPC: https://grpc.io
- Go: https://go.dev









