Was ist Go und warum gehört Googles Sprache zu meinen Backend-Favoriten?
Mich persönlich reizte die Sprache Go bereits vor vielen Jahren als Junior Entwickler, der damals noch in einer Leipziger Agentur mit PHP im Backend arbeitete. Googles neue Sprache versprach eine niedrige Einstiegshürde und schnelles Entwickeln produktiver Anwendungen bei exzellenter Performance, Robustheit und Qualität. Heute sehe ich diese Punkte bestätigt und zähle Go zu den besten Programmiersprachen für Backendanwendungen im Webbereich.
Laufzeitverhalten
Als ersten Punkt gilt es festzuhalten: Go verügt über ein exzellentes Laufzeitverhalten, die Performance spielt in vielen Belangen in der Liga von C und C++. Und vor allem entspricht das Startverhalten auch dem, was wir von C oder C++ kennen, da der Code statisch kompiliert in Binaries vorliegt und keine VM gestartet werden muss. Auch hat Go einen recht geringen Speicherbedarf, was die Sprache ebenfalls zu einer guten Alternative zu C und C++ macht. Lediglich der Garbage Collector kann gelegentlich zu den bekannten Verzögerungen führen, die aber in sehr vielen Anwendungsfällen nicht relevant sein werden.
Einfachheit
Worin sich Go grundlegend von C und vor allem auch von C++ unterscheidet, das ist die Einfachheit der Sprache: So reduziert der Verzicht auf funktional redundante Sprachmittel (z.B. einmal for
statt while…do
, do…while
, foreach
) die Sprache schon ganz erheblich. Go kennt zwar Pointer, aber keine Pointer-Arithmetik. Es sind keine Überladungen möglich und es gibt auch nicht das in C++ berüchtigte Undefined Behaviour. Ganz allgemein zeichnet sich Go auch durch einen Verzicht auf die klassischen Sprachmittel der Objektorientierung aus. So gibt es keine Klassen, sondern nur Strukturen, die Kapselung von Daten ist nicht auf der Ebene der Strukturen, sondern der Module (packages) möglich. Go setzt auf Komposition statt Vererbung, was so manchen architektonischen Fehler von vornherein ausschließt.
Go verfügt über einen Garbage Collector, der Entwickler von rein technischen Überlegungen entlastet und so ermöglicht, dass wir uns auf das Wesentliche konzentrieren. Polymorphie wird möglich über Interfaces mit dynamischer Bindung an konkrete Implementierung zur Laufzeit (Ducktyping). Eine Struktur kann dadurch ein Interface implementieren, ohne von ihm zu wissen. Das macht lose Kopplung (loose couling) kinderleicht, da es von architektonischer Vorsicht entlastet. Beides kommt aber mit einer gewissen Einschränkung: Denn auch in Go sind Heap-Allokationen und Polymorphie nicht kostenfrei zu haben; sie wirken sich durchaus auf das Laufzeitverhalten aus. Wer eine hochperformante Applikation erstellen möchte, wird beides reduzieren und eine ähnliche Sorgfalt an den Tag legen müssen, wie dies in C++ auch der Fall ist.
Generics
Was mir lange ein Dorn im Auge war, das war die Unmöglichkeit generischen Programmierens, die erst nachträglich mit Version 1.19 und nach langer Diskussion in der Form von Generics eingeführt wurde. Der Streitpunkt war recht klar: Wenn die Sprache generisches Programmieren ermöglichen soll, dann steigt die Dauer des Kompilierens oder die Ausführungszeit – je nach Art der Umsetzung. Und wer C++ und seine Metaprogrammierung aus großen Projekten kennt, der weiß auch um die Gefahren, die durch übermäßig generische Ansätze dort lauern. Gutes generisches Programmieren erfordert viel Sorgfalt. Golang soll aber auch ohne große Sorgfalt geschriebenen Code beherrschbar halten. Stattdessen wurde daher zunächst auf Reflection gesetzt. Ich halte die Entscheidung, Generics und damit eine Art Meta-Programming einzuführen, für goldrichtig. Reflection ist nicht nur deutlich komplizierter und außerdem zur Laufzeit langsamer. Reflection ist auch unleserlich und führt genau zu dem Effekt, der vermieden werden sollte: Sie lenkt von der eigentlichen Aufgabe ab und flutet den Code mit technischen Details aus der Sprache, die nur für Eingeweihte verständlich sind. Generisches Programmieren bietet hingegen die Möglichkeit, kurz, prägnant und expressiv zu sein. Letzteres ist ja beispielsweise der Vorteil der Algorithmen in der Standard-Template-Library von C++.
Tooling
Zu den Vereinfachungen, die einen erheblichen Impact auf ein gutes Ergebnis und qualitativ hochwertigen Code haben, gehören die von Haus aus mitgelieferten Werkzeuge für Test- und Benchmark-Frameworks. Damit entfällt eine weitere Ausrede, Code ohne Unit-Tests zu schreiben oder die Performance-Optimierung ohne empirische Unterfütterung zu betreiben. Neuerdings unterstützt Go sogar nativ Fuzz-Tests, also die Ausführung von Test-Szenarien mit zufällig variierten Eingaben, um unentdeckte Schwachstellen und Bugs zu finden. Von einigen berüchtigten Sicherheitslücken der letzten Jahre ist bekannt, dass Fuzz-Testing sie zuverlässig verhindert hätte (siehe als Beispiel den Fall Heartbleed). Ein leicht zu aktivierender Race-Detector macht auf Fehler in der nebenläufigen Programmierung aufmerksam.
Aus meiner Erfahrung nicht zu unterschätzen ist auch, dass die Entwickler von Go sich für eine einheitliche Formatierung als globale Vorgabe entschieden und den passenden Formatter gleich mitgeliefert haben. Ich weiß nicht, wie viele Stunden ich bereits in Diskussionen über Umbrüche, Einrückungen und Whitespaces verbracht habe. Was ich sicher weiß ist, dass jede dieser Stunden eine zuviel war.
Aktualität
Als recht junge Sprache enthält Go native Unterstützung für die Dinge, die heute oft benötigt werden, etwa für Nebenläufigkeit mit Goroutines, Channels und eingebautem Race-Detector. Channels ermöglichen Nebenläufige Programmierung im Pipeline-Stil. In anderen Sprachen ist sowas mitunter aufwendig zu implementieren und bedarf oft weiterer Bibliotheken. Interessant ist die native Unterstützung von Netzwerkkommunikation über TCP. Leicht implementierbare Schnittstellen vereinfachen die Erstellung lose gekoppelter Services. Golang ist für den Einsatz in Microservices hervorragend geeignet und damit für skalierbare Netzwerkdienste, für Cloud-Computing und Cluster-Computing.
leichte Handhabbarkeit, einfaches Deployment
Einem Mythos zufolge entstand das Konzept zu Go bei Google in der Zeit, in der einziges C++-Projekt kompiliert wurde, als Reaktion auf die langen Zwangspausen in Folge klassischer Build-Prozesse. Und eines ist ganz offensichtlich gelungen: Bei kaum einer kompilierten Sprache ist der Buildprozess so schnell wie bei Go. Hinzu kommt die einfache und einheitliche Einbindung von Abhängigkeiten. In derselben Zeit, in der ich in C++ GoogleTest mit Cmake eingebunden habe, kann ich in Go fast schon einen ersten Prototypen erstellen, zumindest aber den ersten Unit-Test schreiben.
Go-Projekte kompilieren zu je einem einzelnen Executable, das keine komplizierte Anbindung an dynamisch gelinkte Bibliotheken und auch keine virtuelle Maschine benötigt. Damit ist das Deployment so einfach, dass sich die Containerisierung oft erübrigt. Im Vergleich zu einem C++-Projekt lassen sich da schnell mehrere Entwicklertage einsparen.
Der ultraschnelle Build-Prozess und das leichte Deployment machen Go auch zu einer Alternative für Scriptsprachen wie PHP. Denn bisher ist ein wesentlicher Vorteil von PHP, dass damit sehr schnell Prototypen erstellt und ausgerollt und auch Korrekturen ebenso schnell verfügbar gemacht werden können.
Sicherheit
Gerade wer bisher mit dynamisch typisierten oder nicht typisierten Sciptsprachen arbeitete, bemerkt schnell die Strenge der statischen Typisierung in Go. Jede Typumwandlung muss in Go explizit gemacht werden, nirgends wird implizit etwa aus einem uint64
ein uint32
. Was manchen PHP-Entwicklern zunächst schwerfallen mag, ist letztlich ein Segen, denn es vermeidet häufige Fehlerquellen und schwer zu interpretierenden Code, in dem niemand sagen kann, welche Variable zur Laufzeiten Daten welchen Typs beinhalten wird.
Eine Besonderheit ist die Möglichkeit der Deklaration abgeleiteter Typen, die im Gegensatz zu den typedef
- oder using
-Deklarationen in C++ keine bloßen Aliase darstellen, sondern eigene Datentypen. Damit kann bereits der Compiler Verwechslungen bei der Variablenzuordnung ausschließen. In C++ gibt es dafür das Konzept der Strong-Types, das jedoch einer eigenständigen Implementierung bedarf (siehe dazu etwa github.com/alexgunkel/strongtypes).
Gegenüber C und C++ erweist es sich als Sicherheitsgewinn, dass das Speichermanagement nicht mehr von den Entwicklerinnen und Entwicklern übernommen werden muss, sondern ein Garbage Collector den allokierten Heap-Speicher wieder freigibt. Auch die berüchtigten Dangling References sind in Go ausgeschlossen, da sich auch hier die Speicherverwaltung um die korrekte Lifetime der Entitäten kümmert. Ein weiteres Sprachmittel wird Menschen, die bereits mit C++ gearbeitet haben, schnell vertraut sein: defer
als Substitut für RAII ist ein wichtiges Pendant zum Speichermanagement, das die Ausführung einer Funktion beim Verlassen einer sie umgebenden Funktion garantiert. Es führt damit eine Garantie im Rahmen des Stack Unwinding ein ähnlich der Garantie in C++, den Destruktor eines jeden Objekts zum Ende seiner Lifetime auszuführen. Wichtig ist dies insbesondere zur Vermeidung von Resourcenlecks, denn der Speicher ist nur eine, nicht die einzige Quelle solcher Bugs.
Eigensinnige Fehlerbehandlung
Etwas eigensinnig ist der Umgang mit zur Laufzeit auftretenden Fehlern. Man denke hier etwa an abbrechende Netzwerkverbindungen, zu geringen Speicherplatz oder unerwartete Nutzereingaben.
Bekanntlich verwendet Go ganz bewusst keine Exceptions, da Fehler keine Ausnahmesituationen in Programmabläufen seien, sondern ganz normaler Bestandteil von Programmabläufen. Als Ersatz für Exceptions verfügt Go über ein anderes, sehr interessantes Feature: Multiple Rückgabewerte erlauben es, sehr einfach ein Ergebnis von einem aufgetretenen Fehler sauber zu trennen, ohne auf Konstrukte wie Output-Parameter zurückgreifen zu müssen (wie in C üblich). Mehr zu diesem Thema gibt es demnächst hier in einem eigenständigen Text zur Fehlerbehandlung in Go.
Fazit
In Go wird verhindert, dass Entwickler versuchen, zu clever zu sein. Das ist sicherlich ein großer Vorteil der Sprache. Von Anfang an war es auch ein Ziel, dass Entwickler keinen großen Schaden im Projekt anrichten können, indem beispielsweise auf Vererbung und Funktionsüberladung verzichtet wurde, also auf Sprachmittel, die die Wartbarkeit einer Applikation bei falscher Anwendung schnell einschränken. Dadurch ist Go auch für Teams mit sehr unterschiedlichem Erfahrungs- und Kenntnisstand sehr gut geeignet, was in der heutigen Softwareentwicklung die Regel ist. Schon dadurch sollte Golang verstärkt in Betracht gezogen werden.
Vieles an der Sprache ist neu und erfordert von Entwicklerinnen und Entwicklern ein Umdenken und Anpassen vertrauter Strategien. Wer aus einer OOP-Sprache kommt und an Exceptions gewöhnt ist, muss sich auf Kapselung auf Modulebene und eine völlig andere Fehlerbehandlung einstellen. Aus meiner Sicht ist dieser Punkt nicht zu unterschätzen, denn die oft gepriesene Einheitlichkeit und Qualität ist mitnichten ein Selbstläufer. Es braucht motivierte Entwicklerinnen und Entwickler, die bereit sind, sich auf eine manchmal eigenwillige Sprache einzulassen und idiomatische Wege zu finden. Für solche Menschen bietet Go die Möglichkeit, sich voll und ganz auf die Aufgabe zu konzentrieren und mit wenig Ablenkung durch Finessen robuste, performante und wartbare Software zu entwickeln.
Go lässt sich auf allen gängigen Systemen kompilieren und ist letztlich für alles geeignet, was auf einem Server läuft. Für Webapplikationen ist Go inzwischen fester Bestandteil meines favorisierten Tech-Stacks, der sich primär aus einem reaktiven Typescript-Framework im Frontend und einer Kombination aus Go und C++ im Backend zusammensetzt. Damit wird es leicht, eine kurze Entwicklungszeiten mit hoher Qualität und Sicherheit sowie herausragender Performance zu verbinden.