Go: Rückblick nach einem Jahr

von Tobias Kündig

    Das Wichtigste in Kürze

  • Weniger Produktivität, mehr Stabilität
    Im Vergleich zu Frameworks wie Laravel ist die Arbeit mit reinem Go weniger effizient, als Lohn für den Mehraufwand erhält man jedoch eine viel stabilere und performantere Software.
  • Die Arbeit mit Datenbanken ist komplex
    Beim Arbeiten mit Datenbanken gibt es noch Optimierungspotential: Die statische Natur von Go macht die Implementierung von ORMs schwierig.

Unser Rückblick nach einem Jahr

Seit rund einem Jahr setzen wir die Programmiersprache «Go» aktiv in unseren Projekten ein. Neben einigen internen Tools, arbeiten wir seit einigen Monaten auch an der Umsetzung einer neuen Version unserer Pflegesoftware «CareSuite», deren Backend vollständig in Go realisiert wird.

In diesem Blog-Beitrag möchte ich einige (unerwartete) Learnings zu Go erwähnen, welche wir im vergangenen Jahr gemacht haben.

Go im Alltag

Weniger Produktivität, mehr Stabilität

Bei der Entwicklung von Web-Applikationen setzen wir auf Laravel, bzw. auf das October CMS, welches auf Laravel basiert. Mit dieser «Kombo» lassen sich selbst komplexe Webapplikationen innert wenigen Tagen umsetzen ‒ Admin-Panel, Benutzerkonten, Frontend und Schnittstellen sind im Handumdrehen realisiert.

Diese gewohnte Produktivität durch vorgefertigte Bauteile sucht man in Go vergeblich: Zwar bietet die Standard-Bibliothek eine gute Starthilfe für die Entwicklung von Webapplikationen, funktionsreiche Frameworks sind jedoch auch nach 10 Jahren immer noch selten. Dies ist primär durch die statische Natur von Go bedingt, welche sich bis zur Einführung von generischen Typen nicht ändern wird.

Wer ein Go-Framework à la Rails oder Laravel sucht, ist mit Buffalo am besten bedient. Wer lieber etwas mehr Handarbeit betreibt, kann sich seine Applikation auch aus den zahlreichen, qualitativ hochwertigen Go-Bibliotheken zusammenstellen.

Wir haben uns für die nächste Version der CareSuite-Pflegesoftware für Letzteres entschieden. Die Produktivität nimmt damit spürbar ab: Man kümmert sich wieder um grundlegene Applikationsarchitektur, Sessions, Caches, Queues und alles, was einem ein Framework ansonsten «gratis» zur Verfügung stellt.

Wir sehen dies jedoch für die Langlebigkeit und die Performance der Software als klaren Vorteil: Damit sind wir nicht mehr vom Release-Zyklus eines Frameworks abhängig, müssen uns nicht an vorgegebene Framework-Konventionen halten, kennen jeden Teil der Applikation bis ins letzte Detail (es gibt keine «Blackboxes») und jede Zeile Code im Projekt wird auch effektiv benötigt.

Über lange Zeit gesehen versprechen wir uns also mehr Effizienz und Stabilität bei der Entwicklung mit Go.

Arbeiten mit Datenbanken

ORMs lassen zu wünschen übrig

Durch die statische Natur von Go gibt es derzeit keine vergleichbar guten ORMs, die den Datenbank-Zugriff erleichtern.

Projekte wie GORM sind sehr beliebt und funktionieren für viele Anwendungsfälle gut. Wenn die Anforderungen jedoch komplexer werden, stellt sich GORM schnell als komplexe Blackbox heraus, die sehr unberechenbar agiert. So entstehen beispielsweise unter gewissen (eigentlich einfachen) Umständen plötzlich ineffiziente SQL-Queries, welche im schlimmsten Fall eine massiv höhere Belastung für Applikation und Datenbank bedeuten kann. Auch stossen anfänglich gut klingende Funktionen wie «Auto-Migrationen der Datenbank» ebenfalls schnell an ihre Grenzen, wenn nur schon einfache Fremdschlüssel-Beziehungen benötigt werden.

Aufgrund der grossen Beliebtheit der Bibliothek, gibt es derzeit zahlreiche offene GitHub Issues, welche leider nur schleppend abgearbeitet werden.

SQLBoiler ist ein weiteres beliebtes Projekt. Im Gegensatz zu GORM setzt SQLBoiler auf Code-Generierung. Dabei werden Models in Form von Go Structs direkt aus einem bestehenden Datenbankschema generiert. Das Resultat ist ein spezifisch für die eigene Applikation erstelltes ORM. Einer der grössten Nachteile von SQLBoiler für uns war, dass generierte Models umständlich erweitert werden müssen, damit eigene Anpassungen beim erneuten Generieren des ORMs nicht überschrieben werden. Ebenfalls fühlt sich die Entwicklung mit SQLBoiler eher «rucklig» an, da bei jedem neuen Feld das ganze ORM neu generiert wird. In der anfänglichen Entwicklungszeit einer Applikation, in der zahlreiche Überarbeitungen der Datenbank stattfinden, ist dies besonders störend.

Ohne ORM geht es auch

Wir haben uns letztendlich dazu entschieden, komplett auf ein ORM zu verzichtenden: sqlx erleichtert das Arbeiten mit MySQL in Go verglichen zur Standardbibliothek bereits markant. Ohne ORM leidet zwar erneut die Produktivität, jedoch wird jede einzelne Query viel bewusster geschrieben, was sich auf lange Sicht auszahlen wird. Für aufwändige Datenbank-Abfragen setzen wir zudem auf squirrel um SQL-Strings zu generieren. Das Ergebnis liest sich schon fast wie «ORM-Code»:

// SQL Query als String generieren, Parameter werden separat zurückgegeben
query, params, _ := squirrel.Insert("users").Columns("name", "email").
                Values("Tobias", "tobias@offline.ch").
                Values("Patrick", "patrick@offline.ch").
                Values("Michael", "michael@offline.ch")

// Generierte Query mit sqlx ausführen
result = sqlx.MustExec(query, params...)

Vorsicht bei NULL-Werten

In Go besitzt jeder Typ ein so genannter «Null-Wert». Dieser bezeichnet den Standard-Wert einer Variable, wenn sie ohne expliziten Wert deklariert wird. Anders als bei anderen Programmiersprachen können Variablen in Go nicht auf NULL gesetzt werden. Deshalb erhalten sie einen vom Typ abhängigen Null-Wert zugewiesen. Booleans sind beispielsweise standardmässig False, Integer 0 und Strings "". Auch spezielle Typen wie time.Time haben einen Null-Wert. Im Fall von time.Time ist es beispielsweise 0001-01-01 00:00:00 +0000 UTC.

Dies stellt den Entwickler bei der Arbeit mit NULL-Werten aus der Datenbank vor eine Herausforderung: Wird ein NULL-Wert aus der Datenbank in eine int Variable übertragen, konvertiert Go diesen zum Standardwert des Typs, in diesem Fall also 0. Um diese unerwünschte Konvertierung zu umgehen, muss eine Variable als Pointer *int deklariert werden. So kann die Variable neben den üblichen Werten auch auf nil (=NULL in Go) gesetzt werden. Dies ermöglicht die unveränderte Übernahme des NULL-Werts aus der Datenbank. Pointer führen hierbei jedoch zu komplexerem Code und erhöhen das Fehlerpotential.

Mit dem null-Package lässt sich dieses Problem umgehen: NULL-Felder werden mit den speziellen Datentypen deklariert, das null-Package übernimmt dann alle Konvertierungslogik gegenüber der Datenbank:

type User struct {
      ID        int       `json:"id"`
      // Als time.Time wird UpdatedAt immer `0001-01-01 00:00:00 +0000 UTC` sein
      // Als *time.Time kann das Feld NULL sein, das Handling wird jedoch komplizierter
      // Der null.Time Typ übernimmt das Handling des "nullable"-Wertes
      UpdatedAt null.Time `json:"updated_at"`
}

Ausblick auf Teil 2

Im zweiten Teil dieses Beitrags werden wir auf unsere Learnings zu den Themen Testing, Applikationsarchitektur und auf das Go-Ökosystem eingehen.

Du findest den zweiten Teil in einigen Wochen in unserem Blog.

Mehr aus dieser Serie
Go

Tutorials, Informationen und Gedanken zur Go-Programmiersprache.

Weitere Beiträge anzeigen »

Los geht's!

Kontaktiere uns noch heute

Für Offerten, technische Anfragen oder einfach nur um Hallo zu sagen.

Wir verwenden Cookies um die Performance unserer Website zu messen. Möchtest du diese Cookies akzeptieren?